phase-6: typescript introduction

This commit is contained in:
Sai Kiran Vella
2026-06-04 22:16:48 -04:00
parent 96e2840b9b
commit c1b3b26caa
69 changed files with 2323 additions and 1036 deletions
+3 -2
View File
@@ -1,7 +1,8 @@
<script>
<script lang="ts">
import Spinner from '$lib/Spinner.svelte';
import type { SidebarState } from '$lib/types.js';
let { sidebar, onClose } = $props();
let { sidebar, onClose }: { sidebar: SidebarState; onClose: () => void } = $props();
</script>
{#if sidebar.open}
+14 -3
View File
@@ -1,10 +1,21 @@
<script>
import { sigOrd, sorted, verdictShort, vClass } from '$lib/utils.js';
<script lang="ts">
import { sigOrd, sorted } from '$lib/utils.js';
import VerdictPill from '$lib/VerdictPill.svelte';
import SignalBadge from '$lib/SignalBadge.svelte';
import Spinner from '$lib/Spinner.svelte';
import type { AssetType, AssetResult } from '$lib/types.js';
let { type, rows, analyzeLoading = false, onAnalyze } = $props();
let {
type,
rows,
analyzeLoading = false,
onAnalyze,
}: {
type: AssetType;
rows: AssetResult[];
analyzeLoading?: boolean;
onAnalyze: () => void;
} = $props();
// Mode state is self-contained — each table independently tracks inflated vs fundamental
let mode = $state('inflated');
+3 -2
View File
@@ -1,7 +1,8 @@
<script>
<script lang="ts">
import { untrack } from 'svelte';
import type { MarketContext } from '$lib/types.js';
let { ctx, collapsible = false } = $props();
let { ctx, collapsible = false }: { ctx: MarketContext; collapsible?: boolean } = $props();
// Read collapsible once for initial state — untrack avoids a reactive dep on the prop
let expanded = $state(untrack(() => !collapsible));
+3 -2
View File
@@ -1,6 +1,7 @@
<script>
<script lang="ts">
import { fmtPE } from '$lib/utils.js';
let { ctx } = $props();
import type { MarketContext } from '$lib/types.js';
let { ctx }: { ctx: MarketContext } = $props();
// Flat list of chips so the template stays declarative
const chips = $derived([
+3 -2
View File
@@ -1,5 +1,6 @@
<script>
let { signal } = $props();
<script lang="ts">
import type { Signal } from '$lib/types.js';
let { signal }: { signal: Signal | null | undefined } = $props();
const cls = () => {
if (signal?.includes('Strong')) return 'strong';
+2 -4
View File
@@ -1,7 +1,5 @@
<script>
// size: 'sm' | 'md' | 'lg'
// label: optional text shown below (lg only)
let { size = 'md', label = null } = $props();
<script lang="ts">
let { size = 'md', label = null }: { size?: 'sm' | 'md' | 'lg'; label?: string | null } = $props();
</script>
{#if size === 'sm'}
+2 -2
View File
@@ -1,6 +1,6 @@
<script>
<script lang="ts">
import { verdictShort, vClass } from '$lib/utils.js';
let { label } = $props();
let { label }: { label: string | null | undefined } = $props();
</script>
<span class="verdict-pill {vClass(label)}">{verdictShort(label)}</span>
+43 -12
View File
@@ -1,6 +1,19 @@
import type {
ScreenerResult,
MarketContext,
MarketCall,
CalendarEvent,
CatalystStory,
LLMAnalysis,
PortfolioHolding,
PortfolioAdvice,
} from '$lib/types.js';
const BASE = '/api';
export async function screenTickers(tickers) {
// ── Screener ──────────────────────────────────────────────────────────────────
export async function screenTickers(tickers: string[]): Promise<ScreenerResult> {
const res = await fetch(`${BASE}/screen`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -10,13 +23,13 @@ export async function screenTickers(tickers) {
return res.json();
}
export async function fetchCatalysts() {
export async function fetchCatalysts(): Promise<{ tickers: string[]; stories: CatalystStory[] }> {
const res = await fetch(`${BASE}/screen/catalysts`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function analyzeTickers(tickers) {
export async function analyzeTickers(tickers: string[]): Promise<{ analysis: LLMAnalysis | null }> {
const res = await fetch(`${BASE}/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -26,13 +39,23 @@ export async function analyzeTickers(tickers) {
return res.json();
}
export async function fetchPortfolio() {
// ── Finance / Portfolio ───────────────────────────────────────────────────────
export async function fetchPortfolio(): Promise<{
advice: PortfolioAdvice[];
holdings: PortfolioHolding[];
marketContext: MarketContext | null;
netWorth: number | null;
error?: string;
}> {
const res = await fetch(`${BASE}/finance/portfolio`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function addHolding(holding) {
export async function addHolding(
holding: Omit<PortfolioHolding, never>,
): Promise<{ holdings: PortfolioHolding[] }> {
const res = await fetch(`${BASE}/finance/holdings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -42,7 +65,7 @@ export async function addHolding(holding) {
return res.json();
}
export async function removeHolding(ticker) {
export async function removeHolding(ticker: string): Promise<{ holdings: PortfolioHolding[] }> {
const res = await fetch(`${BASE}/finance/holdings/${ticker}`, {
method: 'DELETE',
});
@@ -50,7 +73,7 @@ export async function removeHolding(ticker) {
return res.json();
}
export async function fetchMarketContext() {
export async function fetchMarketContext(): Promise<MarketContext> {
const res = await fetch(`${BASE}/finance/market-context`);
if (!res.ok) throw new Error(await res.text());
return res.json();
@@ -58,19 +81,25 @@ export async function fetchMarketContext() {
// ── Market Calls ──────────────────────────────────────────────────────────────
export async function fetchCalls() {
export async function fetchCalls(): Promise<{ calls: MarketCall[] }> {
const res = await fetch(`${BASE}/calls`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function fetchCall(id) {
export async function fetchCall(id: string): Promise<MarketCall & { current: ScreenerResult }> {
const res = await fetch(`${BASE}/calls/${id}`);
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function createCall(payload) {
export async function createCall(payload: {
title: string;
quarter: string;
thesis: string;
tickers: string[];
date?: string;
}): Promise<MarketCall> {
const res = await fetch(`${BASE}/calls`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -80,13 +109,15 @@ export async function createCall(payload) {
return res.json();
}
export async function deleteCall(id) {
export async function deleteCall(id: string): Promise<{ ok: boolean }> {
const res = await fetch(`${BASE}/calls/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error(await res.text());
return res.json();
}
export async function fetchCallsCalendar(tickers = null) {
export async function fetchCallsCalendar(
tickers: string[] | null = null,
): Promise<{ events: CalendarEvent[] }> {
const url = tickers?.length
? `${BASE}/calls/calendar?tickers=${tickers.join(',')}`
: `${BASE}/calls/calendar`;
+139
View File
@@ -0,0 +1,139 @@
// ── Shared UI types ───────────────────────────────────────────────────────
// Mirror of the server's domain types, used across Svelte components.
export type Signal =
| '✅ Strong Buy'
| '⚡ Momentum'
| '⚠️ Speculation'
| '🔄 Neutral'
| '❌ Avoid';
export type AssetType = 'STOCK' | 'ETF' | 'BOND';
export type ScoreMode = 'inflated' | 'fundamental';
export interface Benchmarks {
marketPE: number | null;
techPE: number | null;
reitYield: number | null;
igSpread: number | null;
}
export interface MarketContext {
sp500Price: number | null;
riskFreeRate: number | null;
vixLevel: number | null;
rateRegime: 'HIGH' | 'NORMAL' | 'LOW';
volatilityRegime: 'HIGH' | 'NORMAL' | 'LOW';
benchmarks: Benchmarks;
}
export interface ScoreResult {
label: string;
score: number;
scoreSummary: string;
audit: {
riskFlags?: string[];
[key: string]: unknown;
};
}
export interface AssetDisplayMetrics {
Price?: string;
Sector?: string;
'P/E'?: string;
PEG?: string;
'ROE%'?: string;
'OpMgn%'?: string;
'FCF Yld%'?: string;
'D/E'?: string;
'Exp Ratio%'?: string;
'Yield%'?: string;
AUM?: string;
'5Y Return%'?: string;
'YTM%'?: string;
Duration?: string;
Rating?: string;
[key: string]: string | null | undefined;
}
export interface AssetResult {
asset: {
ticker: string;
currentPrice: number;
type: AssetType;
displayMetrics: AssetDisplayMetrics;
};
signal: Signal;
inflated: ScoreResult;
fundamental: ScoreResult;
}
export interface ScreenerResult {
STOCK: AssetResult[];
ETF: AssetResult[];
BOND: AssetResult[];
ERROR: Array<{ ticker: string; message: string }>;
marketContext: MarketContext;
}
export interface LLMAnalysis {
summary: string;
sentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
affectedIndustries: Array<{ name: string; reason: string }>;
relatedTickers: Array<{ ticker: string; reason: string }>;
}
export interface SidebarState {
open: boolean;
loading: boolean;
analysis: LLMAnalysis | null;
type: AssetType | null;
error: string | null;
}
export interface PortfolioHolding {
ticker: string;
shares: number;
costBasis: number;
source: string;
type: 'stock' | 'etf' | 'bond' | 'crypto';
}
export interface TickerSnapshot {
price: number | null;
signal: Signal | null;
}
export interface MarketCall {
id: string;
title: string;
quarter: string;
date: string;
thesis: string;
tickers: string[];
snapshot: Record<string, TickerSnapshot>;
}
export interface CalendarEvent {
ticker: string;
type: 'earnings' | 'dividend';
date: string;
[key: string]: unknown;
}
export interface CatalystStory {
title: string;
link: string;
publisher: string;
publishedAt: string;
relatedTickers: string[];
}
export interface PortfolioAdvice {
ticker: string;
action: 'hold' | 'sell' | 'add' | 'watch';
reason: string;
signal: Signal | null;
currentPrice: number | null;
gainLossPct: number | null;
}