c388b6d83c
- Restructured server layer with 5 domains: shared, screener, portfolio, calls, finance - Migrated 58 TypeScript files to domain-driven structure - Updated CLAUDE.md with new architecture documentation - Added .gitignore rules for .md files (except CLAUDE.md) - Removed unused CatalystAnalyst import from app.ts - Fixed lint errors: removed unused imports, fixed regex escape, added console suppressions - Verified no sensitive data in git history - Server code compiles cleanly with TypeScript strict mode
3067 lines
118 KiB
Markdown
3067 lines
118 KiB
Markdown
# CLAUDE.md
|
||
|
||
Guidance for working in this repository.
|
||
|
||
**📋 See [`PHASES.md`](./PHASES.md) for the complete Phase 9-16+ roadmap, architecture summaries, and production readiness checklists.**
|
||
|
||
## Overview
|
||
|
||
`market-screener` is a Node.js project consisting of a Fastify API server that powers the SvelteKit dashboard in the `ui/` subdirectory. **Evolved to support day trading**: real-time news webhooks, LLM-driven stock analysis with prompt caching, multi-user authentication, and Discord alerts for price movements.
|
||
|
||
### Two Scoring Lenses (Original)
|
||
|
||
Every asset is scored under two lenses:
|
||
|
||
- **Market-Adjusted** — gates derived from live Yahoo benchmarks (SPY P/E, XLK P/E, XLRE yield, LQD spread). Reflects what is acceptable in today's market.
|
||
- **Fundamental** — strict Graham/value-investing gates from `ScoringConfig`. Reflects genuine value regardless of market conditions.
|
||
|
||
The comparison produces a **Signal** (Strong Buy / Momentum / Speculation / Neutral / Avoid).
|
||
|
||
### Day Trading Mode (New)
|
||
|
||
The app now supports real-time trading workflows:
|
||
- **News webhooks** (Polygon.io) — ingest market news instantly
|
||
- **Price monitoring** (Alpaca/Interactive Brokers) — detect 5%+ dips
|
||
- **LLM analysis** with prompt caching — analyze opportunities in real-time
|
||
- **Multi-user auth** — JWT-based, role-based portfolio access
|
||
- **Discord notifications** — alerts for trading signals
|
||
- **Trade journal** — log decisions + outcomes for performance analysis
|
||
|
||
ES module project (`"type": "module"`); use `import`/`export`, not `require`.
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
npm install # install dependencies
|
||
npm run dev # start API server (port 3000) + SvelteKit UI (port 5173) together
|
||
npm run server # API server only (port 3000)
|
||
npm test # run all unit tests (node:test, zero external deps)
|
||
npm run test:watch # watch mode — uses verbose spec reporter
|
||
npm run format # format all server/bin/tests with Prettier
|
||
npm run format:check # check formatting without writing (used in CI/pre-commit)
|
||
npm run ui:install # install UI dependencies (ui/ subdirectory)
|
||
```
|
||
|
||
`npm run dev` runs both the API server and the SvelteKit UI (in `ui/`) concurrently. Run `npm run ui:install` once before first use.
|
||
|
||
**Day trading features** (when added):
|
||
```bash
|
||
npm run queue:worker # start BullMQ workers (job processing)
|
||
npm run webhook:test # test webhook signature validation locally
|
||
npm run migrate:db # run schema migrations (when switching SQLite → Postgres)
|
||
```
|
||
|
||
---
|
||
|
||
## Project Structure (Phase 9: Domain-Driven)
|
||
|
||
```
|
||
bin/
|
||
server.ts ← Fastify API server entry point (imports buildApp from server/app.ts)
|
||
|
||
prompts/
|
||
catalyst-analysis.md ← daily catalyst analysis playbook (LLM prompt + workflow)
|
||
|
||
server/
|
||
app.ts ← Fastify app factory (buildApp). Registers CORS + all controllers.
|
||
types.ts ← Barrel export: export * from domains/shared/types
|
||
|
||
domains/ ← Domain-driven architecture (Phase 9+)
|
||
|
||
shared/ ← Infrastructure & cross-domain utilities
|
||
adapters/
|
||
YahooFinanceClient.ts
|
||
AnthropicClient.ts
|
||
SimpleFINClient.ts
|
||
services/
|
||
BenchmarkProvider.ts ← fetches ^GSPC, ^TNX, ^VIX, SPY, XLK, XLRE, LQD → marketContext
|
||
CatalystAnalyst.ts ← fetches Yahoo Finance news, extracts relatedTickers
|
||
LLMAnalyst.ts ← analyzes headlines → summary, sentiment, industries, tickers
|
||
CatalystCache.ts ← 15-minute cache for catalyst analysis (Phase 8j)
|
||
entities/
|
||
Asset.ts, Stock.ts, Etf.ts, Bond.ts
|
||
persistence/
|
||
MarketCallRepository.ts
|
||
PortfolioRepository.ts
|
||
config/
|
||
constants.ts ← SIGNAL, SCORE_MODE, ASSET_TYPE, REGIME, CAP_CATEGORY, etc.
|
||
scoring/
|
||
ScoringConfig.ts ← gates, weights, thresholds (single source of truth)
|
||
MarketRegime.ts ← derives INFLATED overrides from live benchmarks
|
||
db/
|
||
DatabaseConnection.ts ← SQLite wrapper with audit logging
|
||
DatabaseInitializer.ts ← schema, migrations, legacy JSON migration
|
||
QueryAudit.ts
|
||
queries.constant.ts
|
||
utils/
|
||
logger.ts
|
||
Chunker.ts
|
||
QueryBuilder.ts
|
||
sanitizer.ts
|
||
types/
|
||
*.model.ts ← all TypeScript types (asset, market, portfolio, finance, etc.)
|
||
index.ts ← public API barrel
|
||
|
||
screener/ ← Stock/ETF/Bond filtering & scoring
|
||
screener.controller.ts ← POST /api/screen, GET /api/screen/catalysts
|
||
analyze.controller.ts ← POST /api/analyze (LLM analysis)
|
||
ScreenerEngine.ts ← orchestrates: fetch → score × 2
|
||
PersonalFinanceAnalyzer.ts ← net worth, cash vs investments analysis
|
||
scorers/
|
||
StockScorer.ts
|
||
EtfScorer.ts
|
||
BondScorer.ts
|
||
transform/
|
||
DataMapper.ts ← normalises Yahoo payload → flat asset data
|
||
RuleMerger.ts ← merges base rules + sector overrides
|
||
index.ts
|
||
|
||
portfolio/ ← Holdings management & investment advice
|
||
finance.controller.ts ← GET /api/finance/portfolio, POST|DELETE /api/finance/holdings
|
||
PortfolioAdvisor.ts ← cross-references holdings with signals
|
||
index.ts
|
||
|
||
calls/ ← Market call tracking & earnings calendar
|
||
calls.controller.ts ← CRUD for market calls + GET /api/calls/calendar
|
||
CalendarService.ts ← earnings calendar logic
|
||
index.ts
|
||
|
||
finance/ ← Portfolio reporting
|
||
finance.controller.ts ← portfolio metrics endpoint
|
||
index.ts
|
||
```
|
||
|
||
clients/ ← external API connectors, one class per third-party system
|
||
YahooFinanceClient.ts ← wraps yahoo-finance2 v3, retry + backoff. Methods: fetchSummary,
|
||
fetchCalendarEvents, search. Typed via YahooFinanceLib interface.
|
||
SimpleFINClient.ts ← claims setup token → access URL, fetches /accounts via Basic Auth.
|
||
AnthropicClient.ts ← wraps Anthropic SDK. complete(system, user) → raw text response.
|
||
|
||
models/ ← domain entity classes with metrics + display logic
|
||
Asset.ts ← abstract base: ticker, currentPrice, type, formatting helpers
|
||
Stock.ts ← metrics + _mapToStandardSector (8 sectors) + _classifyMarketCap
|
||
(Mega/Large/Mid/Small/Micro) + _classifyGrowth (style classification).
|
||
Holds: valuation, quality, risk, 52W movement, analyst consensus, DCF.
|
||
Etf.ts ← metrics: expenseRatio, yield, volume, fiveYearReturn, totalAssets
|
||
Bond.ts ← metrics: ytm, duration, creditRating, creditRatingNumeric
|
||
|
||
scorers/ ← stateless pure scoring functions, no I/O
|
||
StockScorer.ts ← gate checks + weighted registry:
|
||
core: ROE, opMargin, margin, peg, revenue, fcf
|
||
expert: analyst consensus (inverted Yahoo 1–5 scale), DCF margin of safety
|
||
riskFlags: beta, 52W position, 52W momentum, analyst divergence, DCF divergence
|
||
EtfScorer.ts ← expense gate + registry (cost, yield, volume, fiveYearReturn)
|
||
BondScorer.ts ← credit gate + spread/duration scoring
|
||
|
||
config/
|
||
ScoringConfig.ts ← CREDIT_RATING_SCALE + ScoringRules (single source of truth for all
|
||
gates, weights, thresholds including analyst and dcf weights)
|
||
constants.ts ← SIGNAL, ASSET_TYPE, SECTOR, SCORE_MODE, REGIME, SIGNAL_ORDER,
|
||
CAP_CATEGORY (Mega/Large/Mid/Small/Micro),
|
||
GROWTH_CATEGORY (High Growth/Growth/Stable/Value/Turnaround/Declining)
|
||
|
||
types/ ← all domain types, one file per domain
|
||
asset.model.ts ← Signal, AssetType, ScoreMode, ScoringRules, ScoreResult, AssetResult,
|
||
ScreenerResult, GateSet, WeightSet, ThresholdSet, RuleBlock, StockRules
|
||
market.model.ts ← RateRegime, VolatilityRegime, Benchmarks, MarketContext
|
||
portfolio.model.ts ← HoldingType, PortfolioHolding, PortfolioAdvice, AdviceRow
|
||
calls.model.ts ← TickerSnapshot, MarketCall, SnapshotEntry, CreateCallInput
|
||
finance.model.ts ← LLMAnalysis, CatalystStory, CalendarEvent, SimpleFINAccount,
|
||
SimpleFINTransaction, SimpleFINData, YahooNewsItem, YahooFinanceLib
|
||
logger.model.ts ← Logger
|
||
models.model.ts ← AssetData, StockData, StockMetrics, EtfData, EtfMetrics, BondData, BondMetrics
|
||
repositories.model.ts ← StoreData, PortfolioData
|
||
scorers.model.ts ← NumVal, SanitizedMetrics, SanitizedBondMetrics
|
||
services.model.ts ← BenchmarkProviderOptions, InflatedOverrides, ErrorResult, RuleSet,
|
||
ScreenerEngineOptions, CatalystResult, MappedData, and other service shapes
|
||
schemas.ts ← Fastify JSON Schema objects for request body validation
|
||
index.ts ← re-exports all of the above (use this barrel for imports)
|
||
types.ts ← thin barrel: export type * from './types/index.js'
|
||
|
||
utils/
|
||
Chunker.ts ← splits ticker list into batches
|
||
logger.ts ← noopLogger constant for silencing output in server context
|
||
|
||
ui/ ← SvelteKit dashboard (lives inside this repo, not a separate repo)
|
||
src/
|
||
styles/ ← global SCSS design-token system
|
||
app.scss ← root file — @use all partials
|
||
_tokens.scss ← CSS custom properties generated from SCSS maps ($bg, $text, $blues, $signals…)
|
||
_reset.scss ← box-sizing reset + body base
|
||
_layout.scss ← shell, nav, .links, main, nav-progress, .loading-area
|
||
_section.scss ← .section, .section-header, .count, .mode-tabs, .error-banner
|
||
_table.scss ← table, thead/tbody, .table-wrap, .col-ticker, .ticker, .num, .tag
|
||
_buttons.scss ← button base, .btn-primary, .btn-catalyst, .btn-ghost, .btn-screen, .btn-analyze
|
||
_badges.scss ← .verdict-pill, .sentiment-pill, .text-* helpers (SCSS @each maps)
|
||
lib/
|
||
api.ts ← backward-compat shim; re-exports from api/index.ts
|
||
api/
|
||
screener.ts ← screenTickers, fetchCatalysts, analyzeTickers
|
||
finance.ts ← fetchPortfolio, addHolding, removeHolding, fetchMarketContext
|
||
calls.ts ← fetchCalls, fetchCall, createCall, deleteCall, fetchCallsCalendar
|
||
index.ts ← barrel re-export
|
||
types.ts ← re-exports shared types from $types (server/types/); UI-only types defined here
|
||
utils.ts ← shared pure functions: sigOrd, sorted, verdictShort, vClass, fmtPE, fmt…
|
||
MarketContext.svelte ← collapsible card-grid context (used in portfolio + safe-buys)
|
||
MarketContextStrip.svelte ← horizontal chip strip (used in screener)
|
||
AssetTable.svelte ← STOCK/ETF/BOND section: mode tabs + Analyze + table
|
||
AnalysisSidebar.svelte ← LLM analysis slide-over panel
|
||
VerdictPill.svelte ← verdict-pill span; props: label
|
||
SignalBadge.svelte ← signal emoji + label badge
|
||
Spinner.svelte ← sm: dot-pulse | md/lg: chart-line animation
|
||
routes/
|
||
+page.ts ← SvelteKit load (ssr:false) — fetches catalysts + screens on mount
|
||
+page.svelte ← main screener UI
|
||
+layout.svelte ← shell, nav, nav-progress bar, nav-overlay with Spinner
|
||
calls/ ← market calls list + detail views
|
||
portfolio/ ← portfolio advice view
|
||
safe-buys/ ← filtered strong-buy view
|
||
|
||
market-screener.db ← SQLite database (created on first boot). Contains holdings + market_calls tables.
|
||
Legacy portfolio.json / market-calls.json are auto-migrated on first boot
|
||
and renamed to *.json.migrated.
|
||
.env ← SIMPLEFIN_ACCESS_URL or SIMPLEFIN_SETUP_TOKEN, ANTHROPIC_API_KEY, API_KEY (optional — enables Bearer auth on all routes)
|
||
```
|
||
|
||
---
|
||
|
||
## Data Flow
|
||
|
||
```
|
||
Yahoo Finance API
|
||
↓
|
||
BenchmarkProvider — fetches ^GSPC, ^TNX, ^VIX, SPY, XLK, XLRE, LQD
|
||
builds marketContext { sp500Price, riskFreeRate, vixLevel,
|
||
rateRegime, volatilityRegime, benchmarks { marketPE, techPE, reitYield, igSpread } }
|
||
↓
|
||
DataMapper — normalises raw Yahoo payload → flat data object with type (STOCK/ETF/BOND)
|
||
uses trailingPE as primary; preserves negative FCF yield; infers bond duration
|
||
computes: DCF intrinsic value, analyst upside, 52W movement, grossMargin, marketCap
|
||
↓
|
||
Asset subclass — Stock / Etf / Bond holds metrics + getDisplayMetrics()
|
||
Stock also derives capCategory (Mega/Large/Mid/Small/Micro)
|
||
and growthCategory (High Growth/Growth/Stable/Value/Turnaround/Declining)
|
||
↓
|
||
RuleMerger × 2 — FUNDAMENTAL mode: ScoringConfig as-is (Graham-style)
|
||
INFLATED mode: sector override + MarketRegime live gate overrides
|
||
↓
|
||
Scorer × 2 — StockScorer / EtfScorer / BondScorer, fully stateless
|
||
↓
|
||
ScreenerEngine — derives Signal from comparing both verdicts
|
||
↓
|
||
└── API path: screenTickers() → JSON (with serialized displayMetrics) → SvelteKit UI
|
||
```
|
||
|
||
---
|
||
|
||
## API Routes (Fastify)
|
||
|
||
| Method | Path | Description |
|
||
|---|---|---|
|
||
| GET | `/health` | Health check |
|
||
| POST | `/api/screen` | Screen tickers. Body: `{ tickers: string[] }` (max 50). Returns `{ STOCK, ETF, BOND, ERROR, marketContext }` with `asset.displayMetrics` pre-serialized |
|
||
| GET | `/api/screen/catalysts` | Yahoo news → `{ tickers, stories }` |
|
||
| GET | `/api/finance/portfolio` | Portfolio advice + optional SimpleFIN data |
|
||
| GET | `/api/finance/market-context` | Live benchmark data only |
|
||
| GET | `/api/calls` | List all market calls (newest first) |
|
||
| GET | `/api/calls/:id` | Get one call + re-screened current prices for comparison |
|
||
| POST | `/api/analyze` | Fetch Yahoo news for specific tickers + run LLM analysis. Body: `{ tickers: string[] }`. Returns `{ analysis }` |
|
||
| POST | `/api/calls` | Create a market call; snapshots current prices. Body: `{ title, quarter, thesis, tickers[], date? }` |
|
||
| DELETE | `/api/calls/:id` | Delete a market call |
|
||
| GET | `/api/calls/calendar` | Earnings + dividend calendar. Query: `?tickers=AAPL,MSFT` (omit for all call tickers) |
|
||
|
||
CORS is configured for `CLIENT_ORIGIN` env var (default `http://localhost:5173`).
|
||
|
||
Request body validation uses Fastify JSON Schema, defined in `server/types/schemas.ts`.
|
||
|
||
---
|
||
|
||
## Scoring Modes
|
||
|
||
| Mode | P/E Gate (general) | P/E Gate (tech) | Source |
|
||
|---|---|---|---|
|
||
| FUNDAMENTAL | 15x | 35x | ScoringConfig (true Graham) |
|
||
| INFLATED | S&P 500 P/E × 1.5 | XLK P/E × 1.3 | Live SPY/XLK data |
|
||
|
||
**Rate regime effect on INFLATED mode:**
|
||
- HIGH rate regime: P/E multiplier compresses to 1.2× (vs 1.5× in NORMAL)
|
||
- HIGH rate regime: REIT yield floor tightens (0.95× vs 0.85×)
|
||
- HIGH rate regime: bond spread demand increases (0.90× vs 0.80×)
|
||
|
||
| Signal | Meaning |
|
||
|---|---|
|
||
| ✅ Strong Buy | Passes both fundamental AND inflated gates |
|
||
| ⚡ Momentum | Passes inflated, holds fundamentally |
|
||
| ⚠️ Speculation | Passes inflated, fails fundamental |
|
||
| 🔄 Neutral | Hold territory in one or both lenses |
|
||
| ❌ Avoid | Fails both |
|
||
|
||
---
|
||
|
||
## ScoringConfig Key Values
|
||
|
||
`server/config/ScoringConfig.ts` — single source of truth for all gates, weights, thresholds.
|
||
|
||
**STOCK base gates (Fundamental mode):**
|
||
- `maxPERatio: 15` — Graham's actual rule (trailing P/E)
|
||
- `maxPegGate: 1.0` — Lynch standard: PEG > 1.0 means paying full price
|
||
- `maxDebtToEquity: 1.5` — most distress starts above 2x
|
||
- `minQuickRatio: 0.8` — below this signals real liquidity stress
|
||
|
||
**STOCK base weights:**
|
||
- `roe: 3`, `fcf: 3` — primary quality signals
|
||
- `opMargin: 2`, `margin: 2`, `peg: 2`, `revenue: 2` — secondary factors
|
||
- `analyst: 2` — Wall Street consensus (Yahoo 1–5 scale inverted; requires ≥3 analysts)
|
||
- `dcf: 2` — DCF margin of safety (positive = undervalued; only fires when FCF > 0)
|
||
|
||
**Sector overrides** (structural — apply in both modes):
|
||
|
||
| Sector | Key difference |
|
||
|---|---|
|
||
| TECHNOLOGY | D/E up to 2.0, P/E up to 35x, FCF weight raised |
|
||
| REIT | P/E and PEG disabled (9999), scored on yield + P/FFO |
|
||
| FINANCIAL | D/E disabled, scored on ROE + P/B, maxPriceToBook 1.5x |
|
||
| ENERGY | FCF weight 4, yield weight 3, opMargin primary |
|
||
| HEALTHCARE | Revenue growth primary, P/E up to 25x |
|
||
| COMMUNICATION | FCF weight 4, P/E up to 25x (META, GOOGL, NFLX) |
|
||
| CONSUMER_STAPLES | Margin/ROE focus, low revenue growth expectations |
|
||
| CONSUMER_DISCRETIONARY | Revenue growth primary, P/E up to 25x |
|
||
|
||
**ETF gates:**
|
||
- `maxExpenseRatio: 0.2%` — hard gate
|
||
- `minFiveYearReturn: 8.0%` — S&P long-run floor
|
||
- `minVolume: 1,000,000` ADV
|
||
|
||
**BOND gates:**
|
||
- `minCreditRating: 7` (BBB = investment-grade floor)
|
||
- `minSpread: 1.5%` above risk-free
|
||
- `maxDuration: 7` years
|
||
|
||
---
|
||
|
||
## MarketRegime (INFLATED overrides)
|
||
|
||
`server/services/MarketRegime.ts` derives gate overrides from live benchmarks and current rate regime:
|
||
|
||
| Gate | Formula (NORMAL rates) | Formula (HIGH rates) |
|
||
|---|---|---|
|
||
| Stock maxPERatio | SPY trailing P/E × 1.5 | SPY trailing P/E × 1.2 |
|
||
| Tech maxPERatio | XLK P/E × 1.3 | XLK P/E × 1.3 |
|
||
| Tech maxPegGate | XLK P/E ÷ 15 | XLK P/E ÷ 15 |
|
||
| REIT minYield | XLRE yield × 0.85 | XLRE yield × 0.95 |
|
||
| Bond minSpread | LQD−TNX × 0.80 | LQD−TNX × 0.90 |
|
||
| ETF maxExpenseRatio | 0.75% | 0.75% |
|
||
|
||
Rate regime thresholds: `< 2%` = LOW, `2–5%` = NORMAL, `> 5%` = HIGH (10Y Treasury yield).
|
||
**Known issue**: sharp break at 5% can flip the scoring regime between two back-to-back requests when the 10Y hovers near the threshold. Consider smoothing or making the threshold configurable via env var.
|
||
|
||
---
|
||
|
||
## Expert Scoring Features (Stock)
|
||
|
||
Added to `DataMapper.ts` (extraction) and `StockScorer.ts` (scoring).
|
||
All data comes from Yahoo's existing modules — no extra API calls required.
|
||
|
||
### DCF Intrinsic Value
|
||
|
||
Two-stage discounted cash flow model computed per stock with positive TTM FCF:
|
||
|
||
- **Stage 1**: FCF per share grows at the earnings/revenue growth rate for 5 years, discounted at 9.5% (4% risk-free + 5.5% equity risk premium)
|
||
- **Stage 2**: Terminal value at 2.5% perpetuity growth (Gordon Growth Model)
|
||
- Growth rate capped at 30%, floored at -5%
|
||
- Returns `dcfIntrinsicValue` ($ per share) and `dcfMarginOfSafety` (% undervaluation)
|
||
|
||
Scoring: ≥20% margin of safety → +dcf weight; 0–20% → +1; -20% to 0 → -1; < -20% → -dcf weight.
|
||
Only fires when FCF > 0. Risk flags trigger at ±30% divergence.
|
||
|
||
### Analyst Consensus
|
||
|
||
From `financialData.recommendationMean` (Yahoo scale: 1.0 = Strong Buy, 5.0 = Strong Sell):
|
||
|
||
- Requires ≥3 analysts to avoid noise from thin coverage
|
||
- ≤2.0 → full weight; ≤3.0 → +1; ≤4.0 → -1; >4.0 → -full weight
|
||
- `analystTargetPrice`, `analystUpside` (% to target), `numberOfAnalysts` surfaced in display
|
||
- Risk flags trigger at ≥25% upside or ≤-15% downside vs analyst target
|
||
|
||
### 52-Week Movement
|
||
|
||
Three fields replace the single `52W Pos` position metric:
|
||
|
||
| Field | Meaning |
|
||
|---|---|
|
||
| `52W Chg` | Total % price return over last 52 weeks (`ks['52WeekChange']`) |
|
||
| `From High` | % current price is below 52-week high (negative = below peak) |
|
||
| `From Low` | % current price is above 52-week low (positive = recovered) |
|
||
|
||
Risk flags: strong uptrend (≥+50%), significant drawdown (≤-30%), >20% off 52W high.
|
||
|
||
### Market Cap Segmentation
|
||
|
||
`Stock._classifyMarketCap()` derives `capCategory` from `price.marketCap`:
|
||
|
||
| Tier | Threshold |
|
||
|---|---|
|
||
| Mega Cap | > $200B |
|
||
| Large Cap | $10B – $200B |
|
||
| Mid Cap | $2B – $10B |
|
||
| Small Cap | $300M – $2B |
|
||
| Micro Cap | < $300M |
|
||
|
||
### Growth / Style Classification
|
||
|
||
`Stock._classifyGrowth()` derives `growthCategory` from revenue growth, earnings growth, and dividend yield:
|
||
|
||
| Category | Condition |
|
||
|---|---|
|
||
| High Growth | revenueGrowth ≥ 15% OR earningsGrowth ≥ 20% |
|
||
| Growth | revenueGrowth 5–15% |
|
||
| Value | revenueGrowth < 5% AND dividendYield ≥ 3% |
|
||
| Stable | Low growth, modest or no dividend |
|
||
| Turnaround | earningsGrowth < 0% AND revenueGrowth ≥ 0% |
|
||
| Declining | revenueGrowth < -5% |
|
||
|
||
Both `Cap Tier` and `Style` appear in `getDisplayMetrics()` and the screener table.
|
||
|
||
---
|
||
|
||
## Sector Detection
|
||
|
||
`Stock._mapToStandardSector()` maps Yahoo Finance `sector`/`industry` strings to internal constants.
|
||
Order matters — more specific matches first:
|
||
|
||
```
|
||
TECHNOLOGY → "technology", "electronic", "semiconductor", "software"
|
||
REIT → "real estate", "reit"
|
||
FINANCIAL → "financial", "bank", "insurance", "asset management"
|
||
ENERGY → "energy", "oil", "gas", "petroleum"
|
||
HEALTHCARE → "health", "biotech", "pharmaceutical", "medical"
|
||
COMMUNICATION→ "communication", "media", "entertainment", "telecom"
|
||
CONSUMER_STAPLES → "consumer defensive", "consumer staples", "household", "beverage", "food"
|
||
CONSUMER_DISCRETIONARY → "consumer cyclical", "consumer discretionary", "retail", "apparel", "auto"
|
||
GENERAL → fallback
|
||
```
|
||
|
||
---
|
||
|
||
## DataMapper Notes
|
||
|
||
- **peRatio**: prefers `trailingPE` (audited) over `forwardPE` (analyst estimate, ~10-15% optimistic)
|
||
- **FCF yield**: `freeCashflow !== 0` (not `> 0`) — negative FCF preserved so cash-burning companies fail the gate, not silently skip it
|
||
- **grossMargin**: `financialData.grossMargins * 100` — exposed in display; not yet a scoring factor
|
||
- **marketCap**: `price.marketCap` — used for cap tier classification
|
||
- **analystRating**: `financialData.recommendationMean` (1=Strong Buy, 5=Strong Sell). `targetMeanPrice` and `numberOfAnalystOpinions` also extracted
|
||
- **52W movement**: `defaultKeyStatistics['52WeekChange']` for annual return; `From High`/`From Low` computed from `fiftyTwoWeekHigh`/`fiftyTwoWeekLow`
|
||
- **DCF growth rate**: uses `earningsGrowth` (TTM decimal) first, falls back to `revenueGrowth * 0.7`. Capped at 30%, floored at -5%
|
||
- **Bond duration**: inferred from category string ("Short-Term" → 2y, "Intermediate" → 5y, "Long" → 18y, default 6y). Yahoo does not expose effective duration in the modules we fetch
|
||
- **D/E ratio**: Yahoo returns `financialData.debtToEquity` as a percentage (e.g. 175.56 for 1.7556×); dividing by 100 normalises to the standard ratio
|
||
- **Quick ratio**: falls back to `currentRatio` when missing — known methodological issue; current ratio includes inventory and can overstate liquidity for retailers/manufacturers
|
||
|
||
---
|
||
|
||
## Missing Data Convention
|
||
|
||
- Missing metrics use `null` (not `0`) in `_sanitize`. Gate checks skip `null` rather than auto-failing.
|
||
- `pegRatio` falls back to `trailingPE / earningsGrowth` when Yahoo doesn't provide it.
|
||
- DCF, analyst, and 52W scoring factors are all optional — they activate only when the underlying data is non-null.
|
||
|
||
---
|
||
|
||
## Logger Injection Pattern
|
||
|
||
Classes that produce output accept an optional `{ logger }` constructor option so they work cleanly in server context. Pass `noopLogger` from `server/utils/logger.ts` to silence all output.
|
||
|
||
Affected: `ScreenerEngine`, `BenchmarkProvider`, `CatalystAnalyst`, `SimpleFINClient`, `LLMAnalyst`.
|
||
|
||
```ts
|
||
// CLI (default) — writes to stdout
|
||
new ScreenerEngine()
|
||
|
||
// Server — fully silent
|
||
new ScreenerEngine({ logger: noopLogger })
|
||
```
|
||
|
||
---
|
||
|
||
## SimpleFIN Auth Flow
|
||
|
||
1. User gets a Setup Token from https://beta-bridge.simplefin.org
|
||
2. `SimpleFINClient.init()` base64-decodes it → POSTs once to claim Access URL
|
||
3. `onAccessUrlClaimed` callback is called with the URL — CLI uses `saveAccessUrlToEnv()`, server stores elsewhere
|
||
4. All subsequent requests use Access URL with `Authorization: Basic` header (not embedded in URL)
|
||
|
||
---
|
||
|
||
## Holdings Format
|
||
|
||
Holdings are stored in the `holdings` table in `market-screener.db`. To seed initial data, add holdings via the Portfolio UI or by inserting into the database directly.
|
||
|
||
`type` values: `stock`, `etf`, `bond`, `crypto`. Crypto is priced via Yahoo (BTC-USD style) but not fundamentally scored.
|
||
|
||
If you have an existing `portfolio.json`, it will be auto-migrated to SQLite on first boot and renamed to `portfolio.json.migrated`.
|
||
|
||
---
|
||
|
||
## Tests
|
||
|
||
Uses Node's built-in test runner (`node:test` + `node:assert/strict`) — no test framework needed.
|
||
|
||
```
|
||
tests/
|
||
ScoringConfig.test.js ← gate values (P/E 15x, PEG 1.0, QuickRatio 0.8), sector overrides
|
||
RuleMerger.test.js ← FUNDAMENTAL vs INFLATED modes, sector merging
|
||
MarketRegime.test.js ← inflated overrides including HIGH/NORMAL rate regime variants
|
||
StockScorer.test.js ← gate failures, scoring labels, risk flags
|
||
EtfScorer.test.js ← expense gate, volume penalty, 5Y return scoring
|
||
BondScorer.test.js ← credit gate, spread/duration scoring, unit handling
|
||
DataMapper.test.js ← type detection, PEG computation, trailing PE preference,
|
||
negative FCF, ETF volume, bond duration inference
|
||
PortfolioAdvisor.test.js ← position gain/loss calc, advice signal mapping, BRK.B normalisation
|
||
LLMAnalyst.test.js ← markdown fence stripping, JSON parse correctness
|
||
```
|
||
|
||
Pre-commit hook runs `lint-staged` (Prettier) then `npm test`. Pre-push hook runs `npm test`.
|
||
Test output uses the built-in `spec` reporter.
|
||
|
||
**Key unit:** `ytm` in `Bond.metrics` is stored as a percentage (e.g. `6.5` = 6.5%). `BondScorer._sanitize` divides by 100 before spread calculation.
|
||
|
||
**Coverage gaps (known):**
|
||
- `MarketCallRepository.ts` — covered by `tests/MarketCallRepository.test.ts` using in-memory SQLite
|
||
- `LLMAnalyst.test.js` — tests a local copy of the fence-stripping regex rather than importing from source; will silently drift if the regex changes
|
||
- API controllers (`server/controllers/`) — no integration tests; covered implicitly by manual testing only
|
||
- Expert scoring features (analyst, DCF, 52W) — not yet covered in `StockScorer.test.js`
|
||
- UI components — not tested at the unit level
|
||
|
||
---
|
||
|
||
## Architecture Guide
|
||
|
||
This section is the single reference for where code lives and how to add features. Read this before touching any file.
|
||
|
||
### Server layer map
|
||
|
||
| Folder | Role | Rule |
|
||
|---|---|---|
|
||
| `server/app.ts` | Fastify bootstrap — wires DI, registers controllers | No business logic |
|
||
| `server/controllers/` | HTTP only — parse request, call service, return response | No business logic, no file I/O, no external API calls |
|
||
| `server/services/` | Business logic and orchestration | No HTTP concerns (`req`/`reply`), no direct file I/O |
|
||
| `server/repositories/` | Data persistence — JSON file read/write | No business logic; one class per data file |
|
||
| `server/clients/` | External API connectors — one class per third-party system | No business logic; only I/O and protocol handling |
|
||
| `server/models/` | Domain entity classes — hold metrics and `getDisplayMetrics()` | No I/O; pure data + formatting |
|
||
| `server/scorers/` | Stateless pure scoring functions | No I/O, no state; `score(metrics, rules, marketContext)` only |
|
||
| `server/config/` | Constants and scoring gates/weights | No logic; change numbers here, not in scorers |
|
||
| `server/types/` | TypeScript interfaces and types | No logic; one `*.model.ts` per domain |
|
||
| `server/utils/` | Shared pure utilities | No domain knowledge |
|
||
| `bin/` | CLI entry points | Call into services only; the only place `process.exit()` is allowed |
|
||
|
||
### UI layer map
|
||
|
||
| Path | Role |
|
||
|---|---|
|
||
| `ui/src/routes/*/+page.ts` | SvelteKit load functions — fetch data for the page |
|
||
| `ui/src/routes/*/+page.svelte` | Route component — composition only; minimal logic |
|
||
| `ui/src/lib/api/screener.ts` | Fetch wrappers for `/api/screen*` |
|
||
| `ui/src/lib/api/finance.ts` | Fetch wrappers for `/api/finance/*` |
|
||
| `ui/src/lib/api/calls.ts` | Fetch wrappers for `/api/calls/*` |
|
||
| `ui/src/lib/stores/` | Reactive state shared across components (Svelte 5 runes) |
|
||
| `ui/src/lib/components/` | Generic shared components (VerdictPill, SignalBadge, Spinner) |
|
||
| `ui/src/lib/portfolio/` | Portfolio-specific components |
|
||
| `ui/src/lib/calls/` | Calls-specific components |
|
||
| `ui/src/lib/types.ts` | Re-exports server types via `$types` alias; UI-only types defined here |
|
||
| `ui/src/lib/utils.ts` | Pure formatting and sorting helpers |
|
||
| `ui/src/styles/` | SCSS partials — global design tokens, layout, tables, forms |
|
||
|
||
### Where to put a new type
|
||
|
||
- Shared domain type (used by server + UI): `server/types/<domain>.model.ts` and re-export from `server/types/index.ts`
|
||
- UI-only type (component state, display shape): `ui/src/lib/types.ts`
|
||
- Private implementation detail used only within one file: inline in that file
|
||
|
||
### Where to put new code — decision table
|
||
|
||
| What you're adding | Where it goes |
|
||
|---|---|
|
||
| New API endpoint | `server/controllers/<domain>.controller.ts` + register in `server/app.ts` |
|
||
| Business logic for that endpoint | New method in `server/services/<Domain>.ts` |
|
||
| Call to a new external API | New class in `server/clients/<Service>Client.ts` |
|
||
| New data stored in a JSON file | New class in `server/repositories/<Domain>Repository.ts` |
|
||
| New scoring rule or gate value | `server/config/ScoringConfig.ts` |
|
||
| New market regime override | `server/services/MarketRegime.ts` → `getInflatedOverrides()` |
|
||
| New stock metric (mapped from Yahoo) | `server/services/DataMapper.ts` → `mapStockData()` + `StockData`/`StockMetrics` interfaces in `server/types/models.model.ts` + `server/models/Stock.ts` constructor + `getDisplayMetrics()` |
|
||
| New scoring factor | `server/config/ScoringConfig.ts` (add weight + threshold) + `server/scorers/StockScorer.ts` (add factor to array) |
|
||
| New UI page | `ui/src/routes/<name>/+page.ts` + `+page.svelte` |
|
||
| New UI fetch call | `ui/src/lib/api/<domain>.ts` + re-export from `api/index.ts` |
|
||
| Reactive state shared by >1 component | `ui/src/lib/stores/<domain>.store.ts` |
|
||
| New shared UI component | `ui/src/lib/components/` (generic) or domain subfolder |
|
||
| New global style | `ui/src/styles/_<domain>.scss` + `@use` in `app.scss` |
|
||
|
||
### Conventions
|
||
|
||
- Asset `type` (uppercased: `STOCK` / `ETF` / `BOND`) is the routing key across `DataMapper`, model classes, `SCORERS` map, and `ScoringRules`. Keep it consistent everywhere.
|
||
- Prefer adjusting `ScoringConfig` or `MarketRegime` over hardcoding numbers in scorers.
|
||
- `BenchmarkProvider` caches for 1 hour in memory — cache is lost on server restart. A persistent cache is planned (see Phase 8).
|
||
- All entry points live in `bin/`. Do not add logic there — they call into `services/` and controllers.
|
||
- **Never** call `process.exit()` inside `server/` — only `bin/` may do that.
|
||
- Class instances don't survive `JSON.stringify`. Call `getDisplayMetrics()` server-side before returning from API routes (see `serializeAssets()` in `screener.controller.ts`).
|
||
- Controllers use constructor injection — dependencies are wired in `server/app.ts`, not created inside handlers.
|
||
- The `$types` alias in the UI resolves to `server/types/` — use it instead of duplicating type definitions.
|
||
- Ticker normalisation (`BRK.B → BRK-B`) happens in `YahooFinanceClient.normalise()` and applies to all callers via `fetchSummary()` and `fetchCalendarEvents()`.
|
||
|
||
### Adding a new scoring metric — step-by-step
|
||
|
||
When adding a new data point that flows from Yahoo → scoring:
|
||
|
||
1. Extract field in `DataMapper.mapStockData()` from the Yahoo payload
|
||
2. Add field to `StockData` and `StockMetrics` interfaces in `server/types/models.model.ts`
|
||
3. Add field to `Stock` constructor assignment and `getDisplayMetrics()` in `server/models/Stock.ts`
|
||
4. Add weight + threshold to `ScoringConfig.ts` base weights/thresholds (and any relevant sector overrides)
|
||
5. Add the field to `SanitizedMetrics` and `_sanitize()` in `StockScorer.ts`
|
||
6. Add a factor entry to the `factors` array in `StockScorer.score()`
|
||
7. Add a test case to `StockScorer.test.js`
|
||
|
||
**Warning**: The `scorer.score(asset.metrics as never, ...)` cast in `ScreenerEngine._process()` bypasses TypeScript for the scorer dispatch. If you add a field to `StockMetrics` but forget to add it to `SanitizedMetrics`, the compiler will not catch it — the scorer will silently receive `undefined`. Always update `_sanitize()` when adding metrics.
|
||
|
||
---
|
||
|
||
## Architecture Roadmap
|
||
|
||
### Phases 1–7 ✅ COMPLETE
|
||
|
||
All phases of the original roadmap are done. Summary of what was delivered:
|
||
- Phase 1–4: CLI cleanup, shared utilities, TypeScript migration, SCSS design tokens
|
||
- Phase 5: `+page.svelte` decomposed into `AssetTable`, `AnalysisSidebar`, `MarketContextStrip`, `VerdictPill`
|
||
- Phase 6: Full TypeScript conversion across `server/` and `bin/`
|
||
- Phase 7a–7b: Type domain split, API module split
|
||
- Phase 7f: Server layer restructured to layered architecture (controllers / services / repositories / clients / models / scorers)
|
||
- Phase 7g: Controllers converted to classes with DI, types moved to domain files, `YahooFinanceClient` properly typed
|
||
|
||
**Pending UI work (Phase 7c–7e):**
|
||
- 7c: Decompose `portfolio/+page.svelte` (751 lines) into `AddHoldingForm`, `InlineEditRow`, `AdviceTable`, `AccountsTable`; decompose `calls/+page.svelte` (385 lines) into `CallForm`, `CallCard`, `CalendarSection`
|
||
- 7d: Add `ui/src/lib/stores/` layer — `screener.store.ts`, `portfolio.store.ts`
|
||
- 7e: Extract inline `<style>` blocks from portfolio, calls, and AnalysisSidebar into `_forms.scss`, `_sidebar.scss`, `_calls.scss`, `_portfolio.scss`
|
||
|
||
---
|
||
|
||
### Phase 8 — Server Hardening & Quality
|
||
|
||
Priority order. Complete earlier items before starting later ones.
|
||
|
||
#### 8a — Fix `as never` scorer dispatch in `ScreenerEngine`
|
||
|
||
`scorer.score(asset.metrics as never, ...)` bypasses TypeScript. Replace with a properly typed discriminated union or per-type dispatch so the compiler can verify that `StockMetrics` matches `StockScorer.score()`'s signature. This is the highest-risk cast in the scoring pipeline.
|
||
|
||
#### 8b — Inject dependencies into `ScreenerEngine` and `PortfolioAdvisor`
|
||
|
||
Both classes self-construct their `YahooFinanceClient` and `BenchmarkProvider` in the constructor, making unit testing impossible without monkey-patching. Target:
|
||
|
||
```ts
|
||
export class ScreenerEngine {
|
||
constructor(
|
||
private readonly client: YahooFinanceClient,
|
||
private readonly benchmarkProvider: BenchmarkProvider,
|
||
{ logger }: ScreenerEngineOptions = {},
|
||
) {}
|
||
}
|
||
```
|
||
|
||
Wire in `server/app.ts`. This unblocks proper service-layer unit tests.
|
||
|
||
#### 8c — Controller integration tests
|
||
|
||
Add one Fastify `inject()` smoke test per route using a fixture for `ScreenerEngine.screenTickers()`. Catches schema validation regressions and response shape changes without needing live Yahoo access. Target: `tests/screener.controller.test.js`, `tests/calls.controller.test.js`.
|
||
|
||
#### 8d — Repository tests
|
||
|
||
`MarketCallRepository` has zero test coverage. Add `tests/MarketCallRepository.test.js` using a temp file path (inject via constructor or env var) to test `list`, `create`, `delete`, and concurrent-write safety.
|
||
|
||
#### 8f — Persistent benchmark cache
|
||
|
||
`BenchmarkProvider`'s 1-hour cache is in-memory only — cold start after every restart adds 2–4s latency to the first request. Write the cached `MarketContext` to `.benchmark-cache.json` (or a single-row SQLite table). Read it on boot; only re-fetch if stale.
|
||
|
||
#### 8g — Rate limiting + API key auth ✅
|
||
|
||
`@fastify/rate-limit` registered globally in `server/app.ts` (`global: false`, opt-in per route). `/api/screen`, `/api/screen/catalysts`, and `/api/analyze` each carry `config: { rateLimit: { max: 10, timeWindow: '1 minute' } }`. API key enforced via `onRequest` hook when `API_KEY` env var is set (`Authorization: Bearer <key>`); `/health` and OPTIONS are exempt. **Requires `npm install` after adding `@fastify/rate-limit` to dependencies (done in package.json).**
|
||
|
||
#### 8h — Extract `CalendarService`
|
||
|
||
`CallsController.calendar()` is 80+ lines of inline event construction, date parsing, and sorting — all inside a controller method. Extract to `server/services/CalendarService.ts` to make it testable and keep the controller under 50 lines.
|
||
|
||
#### 8i — SQLite migration for repositories
|
||
|
||
Both `market-calls.json` and `portfolio.json` use `writeFileSync` with no concurrency guard. Two concurrent writes within the same event loop tick will lose one write. Replace with `better-sqlite3` for both repositories: concurrent-write safe, atomic transactions, no extra infrastructure. At current portfolio sizes the footprint is trivial.
|
||
|
||
#### 8j — Cache `CatalystAnalyst` results
|
||
|
||
`CatalystAnalyst.run()` fires fresh Yahoo news queries on every `/api/screen/catalysts` call. Cache the result for 15 minutes. A new `CatalystAnalyst` instance is also created on each call inside `ScreenerController.catalysts()` — hoist it to a class-level singleton wired in `server/app.ts`.
|
||
|
||
#### 8k — Add expert scoring tests
|
||
|
||
Update `StockScorer.test.js` to cover the three new scoring factors: analyst consensus scoring (including the `numberOfAnalysts < 3` guard), DCF margin of safety scoring (positive/negative/null cases), and the new 52W risk flags.
|
||
|
||
#### 8l — Anthropic prompt caching for LLMAnalyst
|
||
|
||
`LLMAnalyst.analyze()` sends a large system prompt on every `/api/analyze` call. Enabling Anthropic prompt caching would cache the static system prompt across calls, reducing latency and token costs significantly.
|
||
|
||
Target: add `cache_control: { type: 'ephemeral' }` to the system prompt message block in `AnthropicClient.complete()` (or in `LLMAnalyst.analyze()` if the system prompt is built there). Use the `anthropic-beta: prompt-caching-2024-07-31` header. The cache has a 5-minute TTL and applies to the longest common prefix of consecutive requests — ideal for the static analysis instructions that never change between calls.
|
||
|
||
See: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
|
||
|
||
---
|
||
|
||
### Phase 9 — Subdomain Restructure: Server Layer Organization
|
||
|
||
**Goal:** Reorganize `server/` from a flat layer-based structure to a domain-driven structure. This improves navigation, reduces cognitive load when onboarding, and makes feature ownership clearer.
|
||
|
||
**Timeline:** 3 weeks. Complete items in order; each is a self-contained commit with passing tests.
|
||
|
||
#### 9a — Create shared infrastructure layer
|
||
|
||
Create the `server/domains/shared/` hierarchy with type-safe foundations:
|
||
|
||
```
|
||
server/domains/shared/
|
||
├── entities/ (models + their types together)
|
||
│ ├── Asset.ts
|
||
│ ├── Stock.ts
|
||
│ ├── Etf.ts
|
||
│ ├── Bond.ts
|
||
│ └── index.ts
|
||
├── adapters/ (external API wrappers, renamed from "clients")
|
||
│ ├── YahooFinanceAdapter.ts (was YahooFinanceClient)
|
||
│ ├── AnthropicAdapter.ts (was AnthropicClient)
|
||
│ ├── SimpleFINAdapter.ts (was SimpleFINClient)
|
||
│ └── index.ts
|
||
├── services/ (cross-domain services)
|
||
│ ├── BenchmarkProvider.ts
|
||
│ ├── CatalystAnalyst.ts
|
||
│ ├── LLMAnalyst.ts
|
||
│ └── index.ts
|
||
├── scoring/ (rules + regime management)
|
||
│ ├── ScoringConfig.ts
|
||
│ ├── GateValidator.ts (NEW — shared gate-check logic)
|
||
│ ├── MarketRegime.ts
|
||
│ └── index.ts
|
||
├── persistence/ (SQLite stores, renamed from "repositories")
|
||
│ ├── MarketCallStore.ts (was MarketCallRepository)
|
||
│ ├── PortfolioStore.ts (was PortfolioRepository)
|
||
│ └── index.ts
|
||
├── types/ (all domain types)
|
||
│ ├── asset.model.ts
|
||
│ ├── finance.model.ts
|
||
│ ├── market.model.ts
|
||
│ ├── portfolio.model.ts
|
||
│ ├── calls.model.ts
|
||
│ ├── logger.model.ts
|
||
│ ├── models.model.ts
|
||
│ ├── [...other models]
|
||
│ └── index.ts
|
||
├── config/ (constants, not business logic)
|
||
│ ├── constants.ts
|
||
│ └── index.ts
|
||
├── utils/ (pure utilities, no domain knowledge)
|
||
│ ├── logger.ts
|
||
│ ├── Chunker.ts
|
||
│ ├── sanitizer.ts
|
||
│ └── index.ts
|
||
├── db/ (database initialization)
|
||
│ └── index.ts
|
||
├── schemas.ts (Fastify request validation)
|
||
└── index.ts (barrel: export all public APIs)
|
||
```
|
||
|
||
**Steps:**
|
||
1. Create all directories and `index.ts` barrels (copy `export`s from existing files)
|
||
2. Move files per the mapping above
|
||
3. Update relative import paths in all moved files
|
||
4. Run `npm test` — all existing tests should pass with no functional changes
|
||
5. Verify `npm run dev` boots successfully
|
||
|
||
**Commit:** `refactor: create server/domains/shared hierarchy`
|
||
|
||
---
|
||
|
||
#### 9b — Extract screener domain
|
||
|
||
Group all screener-related logic into one subdirectory:
|
||
|
||
```
|
||
server/domains/screener/
|
||
├── ScreenerController.ts
|
||
├── ScreenerEngine.ts
|
||
├── PersonalFinanceAnalyzer.ts
|
||
├── scorers/
|
||
│ ├── StockScorer.ts
|
||
│ ├── EtfScorer.ts
|
||
│ ├── BondScorer.ts
|
||
│ └── index.ts
|
||
├── transform/
|
||
│ ├── DataMapper.ts
|
||
│ ├── RuleMerger.ts
|
||
│ └── index.ts
|
||
└── index.ts
|
||
```
|
||
|
||
**Steps:**
|
||
1. Create `server/domains/screener/` structure
|
||
2. Move files from `server/` (controller, engine, analyzer) into this domain
|
||
3. Move `server/scorers/` into `server/domains/screener/scorers/`
|
||
4. Move `DataMapper.ts` and `RuleMerger.ts` to `server/domains/screener/transform/`
|
||
5. Update imports: all now point to `../shared/` for utilities/types/adapters
|
||
6. Update `server/app.ts` to import from `domains/screener`
|
||
7. Run `npm test` — verify all screener tests pass
|
||
|
||
**Commit:** `refactor: extract screener domain`
|
||
|
||
---
|
||
|
||
#### 9c — Extract portfolio domain
|
||
|
||
```
|
||
server/domains/portfolio/
|
||
├── PortfolioController.ts
|
||
├── PortfolioAdvisor.ts
|
||
├── persistence/
|
||
│ ├── PortfolioStore.ts
|
||
│ └── index.ts
|
||
└── index.ts
|
||
```
|
||
|
||
**Steps:**
|
||
1. Create `server/domains/portfolio/` structure
|
||
2. Move files + dependency on shared adapter/services
|
||
3. Update imports in controller + advisor to point to `../shared/`
|
||
4. Verify portfolio routes work with the new import paths
|
||
5. Run `npm test`
|
||
|
||
**Commit:** `refactor: extract portfolio domain`
|
||
|
||
---
|
||
|
||
#### 9d — Extract calls domain
|
||
|
||
```
|
||
server/domains/calls/
|
||
├── CallsController.ts
|
||
├── CalendarService.ts (extract from CallsController if not done in Phase 8h)
|
||
├── persistence/
|
||
│ ├── MarketCallStore.ts
|
||
│ └── index.ts
|
||
└── index.ts
|
||
```
|
||
|
||
**Steps:**
|
||
1. Create `server/domains/calls/` structure
|
||
2. Move `CallsController` and `MarketCallRepository`
|
||
3. If Phase 8h is not done, extract calendar logic into `CalendarService.ts` now
|
||
4. Update imports
|
||
5. Run `npm test` + verify `/api/calls/*` routes
|
||
|
||
**Commit:** `refactor: extract calls domain`
|
||
|
||
---
|
||
|
||
#### 9e — Extract finance domain
|
||
|
||
Minimal domain — just the controller, since `BenchmarkProvider` stays in shared:
|
||
|
||
```
|
||
server/domains/finance/
|
||
├── FinanceController.ts
|
||
└── index.ts
|
||
```
|
||
|
||
**Steps:**
|
||
1. Create `server/domains/finance/`
|
||
2. Move `FinanceController` (was `finance.controller.ts`)
|
||
3. Update to import `BenchmarkProvider` from `../shared/services`
|
||
4. Verify `/api/finance/*` routes work
|
||
5. Run `npm test`
|
||
|
||
**Commit:** `refactor: extract finance domain`
|
||
|
||
---
|
||
|
||
#### 9f — Clean up old `server/` directories
|
||
|
||
Now that all code is in `domains/`, remove the old flat structure:
|
||
|
||
```bash
|
||
rm -rf server/controllers/
|
||
rm -rf server/services/
|
||
rm -rf server/repositories/
|
||
rm -rf server/clients/
|
||
rm -rf server/models/
|
||
rm -rf server/scorers/
|
||
rm -rf server/config/
|
||
rm -rf server/types/
|
||
rm -rf server/utils/
|
||
```
|
||
|
||
(These now exist under `server/domains/shared/` and individual domains.)
|
||
|
||
**Update `server/app.ts`:**
|
||
|
||
```typescript
|
||
import { buildApp } from './app.ts';
|
||
|
||
// Controllers from domains
|
||
import { ScreenerController } from './domains/screener';
|
||
import { PortfolioController } from './domains/portfolio';
|
||
import { CallsController } from './domains/calls';
|
||
import { FinanceController } from './domains/finance';
|
||
|
||
// Shared services for wiring
|
||
import {
|
||
YahooFinanceAdapter,
|
||
AnthropicAdapter,
|
||
BenchmarkProvider,
|
||
// ... other imports from domains/shared
|
||
} from './domains/shared';
|
||
```
|
||
|
||
**Steps:**
|
||
1. Delete the 8 old directories
|
||
2. Verify all imports in `app.ts` and remaining files point to `domains/`
|
||
3. Run full test suite: `npm test`
|
||
4. Run `npm run dev` and manually check all API routes
|
||
5. Verify `npm run format:check` passes
|
||
|
||
**Commit:** `refactor: remove old flat server layer structure`
|
||
|
||
**After this commit, `server/` directory tree looks like:**
|
||
|
||
```
|
||
server/
|
||
├── app.ts ← Fastify bootstrap (unchanged role, updated imports)
|
||
├── domains/
|
||
│ ├── shared/ ← Shared infrastructure
|
||
│ ├── screener/ ← Screener feature domain
|
||
│ ├── portfolio/ ← Portfolio feature domain
|
||
│ ├── calls/ ← Market calls feature domain
|
||
│ └── finance/ ← Finance reporting domain
|
||
├── db/ ← Database init (moved to domains/shared/db, but link from server/db can stay for backward compat)
|
||
└── types.ts ← Thin barrel: export type * from './domains/shared/types/index.js'
|
||
```
|
||
|
||
---
|
||
|
||
#### 9g — Update documentation in CLAUDE.md
|
||
|
||
Replace the old "Server layer map" section with the new structure:
|
||
|
||
```markdown
|
||
### Server layer map (Phase 9+)
|
||
|
||
All server logic lives under `server/domains/`:
|
||
|
||
| Domain | Folder | Role | Key Files |
|
||
|---|---|---|---|
|
||
| Screener | `screener/` | Stock/ETF/Bond filtering | `ScreenerEngine.ts`, `scorers/`, `transform/` |
|
||
| Portfolio | `portfolio/` | Holdings mgmt + advice | `PortfolioAdvisor.ts`, `PortfolioStore.ts` |
|
||
| Calls | `calls/` | Market call tracking | `CallsController.ts`, `CalendarService.ts` |
|
||
| Finance | `finance/` | Portfolio metrics + reporting | `FinanceController.ts` |
|
||
| Shared | `shared/` | Adapters, services, types, config | `adapters/`, `services/`, `scoring/`, `entities/` |
|
||
|
||
**New conventions:**
|
||
- Import from domain `index.ts` barrels: `import { ScreenerEngine } from '../screener'`
|
||
- Shared types via barrel: `import type { Stock } from '../shared'`
|
||
- Adapters now called "adapters" (was "clients"); entities grouped with models
|
||
- Repositories renamed to "stores" (`PortfolioStore`, `MarketCallStore`)
|
||
```
|
||
|
||
Update the "Where to put new code — decision table" to reference domain folders:
|
||
|
||
| What you're adding | Where it goes |
|
||
|---|---|
|
||
| New API endpoint | `server/domains/<domain>/<Domain>Controller.ts` + register in `server/app.ts` |
|
||
| Business logic for that endpoint | New method in `server/domains/<domain>/<Service>.ts` |
|
||
| Call to a new external API | New class in `server/domains/shared/adapters/<Service>Adapter.ts` |
|
||
| New data stored in a database table | New class in `server/domains/<domain>/persistence/<Entity>Store.ts` |
|
||
| New scoring rule or gate value | `server/domains/shared/scoring/ScoringConfig.ts` |
|
||
| Shared utility across domains | `server/domains/shared/services/<Service>.ts` |
|
||
|
||
**Commit:** `docs: update CLAUDE.md with Phase 9 architecture`
|
||
|
||
---
|
||
|
||
#### 9h — Smoke test all routes
|
||
|
||
Create a simple integration smoke test that verifies all major routes still work after the restructure. This isn't comprehensive (Phase 8c adds real integration tests), but catches import errors and missing registrations.
|
||
|
||
```bash
|
||
# tests/integration.smoke.test.js (10–15 min effort)
|
||
import test from 'node:test';
|
||
import { buildApp } from '../server/app.ts';
|
||
|
||
test('POST /api/screen works', async () => {
|
||
const app = await buildApp();
|
||
const res = await app.inject({
|
||
method: 'POST',
|
||
url: '/api/screen',
|
||
payload: { tickers: ['AAPL'] },
|
||
});
|
||
assert.equal(res.statusCode, 200);
|
||
const body = JSON.parse(res.body);
|
||
assert(body.STOCK || body.ETF || body.BOND);
|
||
});
|
||
|
||
// ... one quick test per major endpoint
|
||
```
|
||
|
||
**Commit:** `test: add smoke tests for Phase 9 restructure`
|
||
|
||
---
|
||
|
||
### Migration Checklist
|
||
|
||
- [ ] 9a: Create shared hierarchy + run tests
|
||
- [ ] 9b: Extract screener domain
|
||
- [ ] 9c: Extract portfolio domain
|
||
- [ ] 9d: Extract calls domain
|
||
- [ ] 9e: Extract finance domain
|
||
- [ ] 9f: Delete old directories, update `app.ts`
|
||
- [ ] 9g: Update CLAUDE.md documentation
|
||
- [ ] 9h: Add smoke tests + verify `npm run dev` locally
|
||
- [ ] Final: Merge as one feature branch (all 9a–9h commits)
|
||
|
||
### Backward Compatibility
|
||
|
||
No breaking changes to the API or public types. File structure is internal — clients see the same routes and response shapes.
|
||
|
||
### Benefits After Phase 9
|
||
|
||
1. **Navigation**: New developers see which file owns which API endpoint at a glance.
|
||
2. **Code discovery**: "Where's the screener logic?" → `server/domains/screener/`. "Where are the database stores?" → `server/domains/shared/persistence/`.
|
||
3. **Onboarding time**: Cut in half. The folder structure *is* the feature map.
|
||
4. **Feature isolation**: Adding a new domain (e.g., `server/domains/watchlist/`) is now a standard pattern — just follow the template.
|
||
5. **Import hygiene**: Shared code stays in `shared/`; feature-specific code stays in domains. No circular imports.
|
||
6. **Testability**: Each domain can be tested independently (Phase 8b + 8c will plug into this structure cleanly).
|
||
|
||
---
|
||
|
||
## Phase 10 — UI Component Restructure & Clarity
|
||
|
||
**Goal:** Mirror Phase 9 server restructure at the UI layer. Organize Svelte components by domain, split utility files, and improve navigability.
|
||
|
||
**Timeline:** 1 week. Follow the same pattern as Phase 9a–9h.
|
||
|
||
#### 10a — Create `lib/components/` structure
|
||
|
||
```
|
||
ui/src/lib/components/
|
||
├── shared/
|
||
│ ├── Spinner.svelte
|
||
│ ├── VerdictPill.svelte
|
||
│ ├── SignalBadge.svelte
|
||
│ ├── MarketContextStrip.svelte
|
||
│ ├── MarketContext.svelte
|
||
│ └── index.ts
|
||
├── layouts/
|
||
│ ├── SidebarLayout.svelte
|
||
│ ├── TableLayout.svelte
|
||
│ └── index.ts
|
||
├── screener/
|
||
│ ├── AssetTable.svelte
|
||
│ ├── AnalysisSidebar.svelte
|
||
│ └── index.ts
|
||
├── portfolio/
|
||
│ ├── AddHoldingForm.svelte
|
||
│ ├── AdviceTable.svelte
|
||
│ ├── AccountsTable.svelte
|
||
│ └── index.ts
|
||
└── calls/
|
||
├── CallForm.svelte
|
||
├── CallCard.svelte
|
||
├── CalendarSection.svelte
|
||
└── index.ts
|
||
```
|
||
|
||
#### 10b — Split `lib/utils.ts` into `lib/utils/`
|
||
|
||
```
|
||
lib/utils/
|
||
├── formatting.ts (fmtPE, fmt, fmtShort, fmtPct)
|
||
├── sorting.ts (sigOrd, sorted)
|
||
├── verdicts.ts (verdictShort, vClass)
|
||
└── index.ts (barrel re-export)
|
||
```
|
||
|
||
#### 10c — Split `lib/types.ts` into `lib/types/`
|
||
|
||
```
|
||
lib/types/
|
||
├── ui.types.ts (AssetDisplayMetrics, SidebarState)
|
||
├── portfolio.types.ts (AdviceRow, HoldingFormData)
|
||
├── screener.types.ts (if screener-specific types exist)
|
||
├── shared.ts (re-exports from $types/*)
|
||
└── index.ts (barrel re-export)
|
||
```
|
||
|
||
#### 10d — Update all imports in routes + stores
|
||
|
||
1. Fix all `import` statements in `routes/` to use new paths
|
||
2. Run `npm run build` + verify no broken links
|
||
3. Commit: `refactor: update imports for Phase 10 restructure`
|
||
|
||
#### 10e — Extract reusable layout components
|
||
|
||
1. Create `SidebarLayout.svelte` — used by AnalysisSidebar
|
||
2. Create `TableLayout.svelte` — used by portfolio + calls
|
||
3. Reduces component duplication
|
||
4. Commit: `refactor: extract reusable layout components`
|
||
|
||
**Commit:** `refactor: UI Phase 10 — component restructure complete`
|
||
|
||
**Benefits:**
|
||
- New devs instantly find components by domain
|
||
- Utilities grouped by responsibility (easier to locate)
|
||
- Types clearly separated (UI-only vs. shared)
|
||
- Consistent with server Phase 9 — unified mental model
|
||
|
||
---
|
||
|
||
## Phase 10.5 — Professional-Grade Screener UI (Institutional Research Tool)
|
||
|
||
**Goal:** Build a professional-grade screener interface that shows complete investment research capabilities. User sees institutional-quality tools from day one, learns mastery through using professional workflows — not through simplified interfaces. Same tool grows them from newbie to pro, not by hiding information but by organizing it.
|
||
|
||
**Philosophy:** Don't teach beginners by simplifying. Teach by showing complete information, organizing it clearly, and measuring outcomes. User of any proficiency level gets:
|
||
- Advanced filtering (multi-criteria, saved presets, numeric ranges)
|
||
- Forensic tearsheet detail (all metrics, decision framework, peer comparison)
|
||
- Visible decision logic (which gates pass/fail, why verdict is what it is)
|
||
- Performance tracking (backtest signals, decision logging, attribution analysis)
|
||
|
||
**Timeline:** 4-6 weeks (after Phase 10).
|
||
|
||
### 10.5a — UI Architecture: Three-Layer Layout
|
||
|
||
```
|
||
Sidebar (280px) | Main Table (flex) | Tearsheet Panel (420px)
|
||
────────────────┼──────────────────┼──────────────────────
|
||
Advanced │ Compact table │ Forensic detail
|
||
filters │ 10 columns only │ Full metrics
|
||
(left) │ (ticker, price, │ Peer comparison
|
||
│ verdict, score, │ Decision framework
|
||
Quick presets │ P/E, ROE, 52W, │ Risk breakdown
|
||
│ DCF, flags, │ Threshold sensitivity
|
||
│ action) │ (right side-panel)
|
||
```
|
||
|
||
**Key principle:** Main table is *scannable* (minimal), tearsheet is *comprehensive* (on-demand).
|
||
|
||
### 10.5b — Sidebar: Advanced Filtering
|
||
|
||
```
|
||
Filter Group (Verdict, Market Cap, Sector):
|
||
• Preset buttons: All, Strong Buy, Buy, Hold, Avoid
|
||
• Can multi-select (eventually)
|
||
|
||
Custom Filters:
|
||
• P/E Range: 10-25 (numeric input, optional)
|
||
• ROE Min: >15% (numeric input)
|
||
• 52W Dip %: 5+ (numeric input, configurable)
|
||
• Debt/Equity Max: 2.0 (numeric input)
|
||
|
||
Quick Presets (saved screeners):
|
||
• "Value Trap Screen" (low P/E, declining revenue, high D/E)
|
||
• "Growth at Fair Price" (P/E < PEG, ROE > 20%, FCF positive)
|
||
• "Dip Opportunity" (52W dip >5%, verdict not Avoid, analyst Buy)
|
||
• "My Watchlist" (user-curated list)
|
||
|
||
**Future enhancement (Phase 10.5e):** User saves custom presets (SQL persistence).
|
||
```
|
||
|
||
### 10.5c — Main Table: Minimal, Scannable
|
||
|
||
**10 columns (monospace numbers, right-aligned):**
|
||
|
||
| Ticker | Price | Verdict | Score | P/E | ROE | 52W | DCF | Flags | Menu |
|
||
|--------|-------|---------|-------|-----|-----|-----|-----|-------|------|
|
||
| AAPL | $189.50 | Strong Buy | 8.2 | 28.5x | 95.2% | +18.5% | +22% | — | ⋯ |
|
||
| MSFT | $425.30 | Buy | 7.1 | 32.1x | 48.5% | +12.3% | +15% | — | ⋯ |
|
||
| NVDA | $892.15 | Hold | 6.5 | 68.2x | 61.5% | +85.2% | -8% | Peak | ⋯ |
|
||
| XYZ | $28.75 | Avoid | 2.1 | 15.8x | -5.2% | -42.1% | -45% | Decline | ⋯ |
|
||
|
||
**Properties:**
|
||
- Sticky header (always visible when scrolling)
|
||
- Hover row → slight background highlight
|
||
- Click row → opens tearsheet
|
||
- Monospace numbers (scannable, professional)
|
||
- Color-coded metrics (positive = green, negative = red, neutral = gray)
|
||
- Verdict pills with icons (✓ Strong Buy, → Buy, ⊙ Hold, ✕ Avoid)
|
||
- Flags show warnings only (Peak, Decline, etc) — clean
|
||
- Header row: "Filtered: 247 | Strong Buy: 12 | Buy: 34"
|
||
- Sort columns: click header to sort (P/E low→high, ROE high→low, etc)
|
||
|
||
### 10.5d — Tearsheet Panel: Professional Research
|
||
|
||
**Right-side slide-in panel (420px, animates in 0.2s).** Sticky header, scrollable body.
|
||
|
||
**Header:**
|
||
```
|
||
NVDA — NVIDIA Corp $892.15 [X]
|
||
```
|
||
|
||
**Body sections:**
|
||
|
||
1. **Core Metrics (4-grid, color-coded cards):**
|
||
```
|
||
P/E Ratio: 68.5x ROE: 61.5%
|
||
(vs mkt avg 18x) (exceptional)
|
||
|
||
FCF Yield: 2.1% 52W Chg: +85.2%
|
||
(strong) (from low)
|
||
```
|
||
|
||
2. **Valuation Context (comparison table):**
|
||
```
|
||
Metric | NVDA | Sector | S&P500
|
||
────────┼───────┼────────┼────────
|
||
P/E | 68.2x | 24.5x | 18.0x
|
||
PEG | 2.8 | 1.2 | 1.0
|
||
ROE | 61.5% | 15.2% | 12.8%
|
||
```
|
||
|
||
3. **Decision Framework (gate-by-gate breakdown):**
|
||
```
|
||
✓ Quality gate (ROE > 15%) PASS
|
||
✗ Valuation gate (P/E < 35x) FAIL (68x)
|
||
✗ Value gate (PEG < 1.0) FAIL (2.8)
|
||
⚠ Dip gate (52W -5%) FAIL (+85%, at peak)
|
||
|
||
Result: Hold (2/4 gates, borderline)
|
||
```
|
||
|
||
4. **Risk Breakdown (ranked, quantified):**
|
||
```
|
||
⚠ Active Risk Flags
|
||
• Valuation: 68x P/E vs 18x market (278% premium)
|
||
• Momentum: +85% from 52W low (at peak, reversal risk)
|
||
• Growth dependency: Needs 25%+ growth to justify multiple
|
||
• Macro: AI capex cycle uncertainty Q2-Q3 2026
|
||
```
|
||
|
||
5. **Threshold Sensitivity (what-if scenarios):**
|
||
```
|
||
If P/E compresses to 50x: Stock -27% to $650
|
||
If growth slows to 15%: Stock -35% to $580
|
||
If rates rise 100bps: Stock -15% to $760
|
||
```
|
||
|
||
6. **Peer Comparison:**
|
||
```
|
||
Stock | P/E | Growth | ROE
|
||
───────┼──────┼────────┼──────
|
||
NVDA | 68x | 25% | 61.5%
|
||
MSFT | 32x | 12% | 48.5%
|
||
GOOG | 23x | 8% | 18.5%
|
||
```
|
||
|
||
7. **CTA Row (bottom):**
|
||
```
|
||
[Add to Watchlist] [Decision Log]
|
||
```
|
||
|
||
**Design principles:**
|
||
- Uppercase section headers (institutional style)
|
||
- All numbers in monospace (scannable)
|
||
- Color for meaning only (green = good, red = bad, black = neutral)
|
||
- Subtle borders between sections (0.5px, tertiary color)
|
||
- No decoration, no gradients, no shadows
|
||
- Typography: 12px body, 16px section title, 18px metric value
|
||
|
||
### 10.5e — Decision Logging & Backtest (Phase 10.5 Extensions)
|
||
|
||
**CTA Button: "Decision Log"** → Opens modal with:
|
||
|
||
```
|
||
Decision Log for NVDA
|
||
|
||
Your thesis:
|
||
[Text area: "Bought at $892, thesis is AI cycle, monitor growth guidance"]
|
||
|
||
Entry date: 2026-06-06
|
||
Entry price: $892.15
|
||
Suggested position: 3% of portfolio
|
||
|
||
Track these 30/60/90 days:
|
||
□ Did dip thesis play out (stock up 10%+)?
|
||
□ Did analyst estimates revise (up or down)?
|
||
□ Did margins stay stable (within ±2%)?
|
||
□ Did revenue guidance hold?
|
||
|
||
[Save Decision]
|
||
```
|
||
|
||
After 30 days, shows:
|
||
```
|
||
NVDA Decision Review (30 days later)
|
||
|
||
Your thesis: "AI cycle"
|
||
Outcome: Stock +18%, analyst estimates +5% EPS, margins stable
|
||
Result: Thesis intact, position profitable
|
||
|
||
Learnings:
|
||
• Your P/E premium call was wrong (stock appreciated despite 68x P/E)
|
||
• Analyst revisions more important than absolute P/E
|
||
• Your "margin stability" tracking was useful signal
|
||
```
|
||
|
||
This builds pattern recognition. After 20-30 decisions logged + reviewed, user starts seeing what *actually* predicts returns.
|
||
|
||
**Backtest view (future, Phase 10.5e):**
|
||
```
|
||
Signal Accuracy Over Time
|
||
|
||
Strong Buy signals: 12 | Correct: 8 (67%) | Avg return: +18%
|
||
Buy signals: 34 | Correct: 21 (62%) | Avg return: +8%
|
||
Hold signals: 56 | Correct: 38 (68%) | Avg return: +2%
|
||
Avoid signals: 28 | Correct: 25 (89%) | Avg return: -12%
|
||
|
||
Best signal: Strong Buy + Dip >10% + Analyst Buy (76% win rate)
|
||
Worst signal: Hold + At Peak (48% win rate)
|
||
|
||
Your decisions vs signal:
|
||
• How often you followed recommendations
|
||
• When you deviated, what happened
|
||
• Attribution: Was it luck or skill?
|
||
```
|
||
|
||
### 10.5f — Implementation (Phased)
|
||
|
||
**Phase 10.5a (Week 1-2): Core UI**
|
||
- Sidebar filters + preset buttons
|
||
- Main table (10 columns, sortable)
|
||
- Tearsheet panel (slides in on row click)
|
||
- Color coding + professional styling
|
||
- Sticky header, monospace numbers
|
||
|
||
**Phase 10.5b (Week 2-3): Tearsheet Sections**
|
||
- Core metrics cards
|
||
- Valuation comparison table
|
||
- Decision framework (gate breakdown)
|
||
- Risk breakdown with quantified risks
|
||
- Threshold sensitivity (if-then scenarios)
|
||
- Peer comparison
|
||
|
||
**Phase 10.5c (Week 3-4): Interactivity**
|
||
- Column sorting (click header)
|
||
- Filter application (sidebar controls table)
|
||
- Tear sheet smooth animation
|
||
- Responsive layout (sidebar collapses on mobile)
|
||
|
||
**Phase 10.5d (Week 4-5): Decision Logging**
|
||
- Decision Log modal
|
||
- Save thesis + entry date/price
|
||
- Track 30/60/90 day outcomes
|
||
- Simple review modal (did thesis play out?)
|
||
|
||
**Phase 10.5e (Week 5-6): Backtest Dashboard (Optional, can defer)**
|
||
- Signal accuracy rates
|
||
- Win rate by signal type
|
||
- User decision attribution
|
||
- Correlation between signals and actual outcomes
|
||
|
||
### 10.5g — Key Features for Pro Growth
|
||
|
||
**What teaches mastery:**
|
||
|
||
1. **Decision Framework visible** — User sees which gates matter for which sectors
|
||
- After 10 decisions: "I notice 'Dip' gate almost always works"
|
||
- After 20 decisions: "Growth gates matter more for tech, quality gates matter more for staples"
|
||
- After 50 decisions: "I'm refining my thresholds"
|
||
|
||
2. **Threshold sensitivity shows downside** — User stops buying at peaks
|
||
- "If P/E → 50x, this stock -27%"
|
||
- "If growth slows, this stock -35%"
|
||
- User thinks: "Is my thesis strong enough to weather that?"
|
||
|
||
3. **Peer comparison normalizes expectations** — User stops overpaying for "quality"
|
||
- "NVDA 68x P/E but MSFT 32x P/E with similar quality"
|
||
- "Understand the premium. Can it sustain?"
|
||
|
||
4. **Decision logging forces reflection** — User learns what *actually* works
|
||
- After first 5: "I bought too many at peaks"
|
||
- After 15: "My 'Dip' picks outperform my 'Strong Buy' picks"
|
||
- After 30: "I should weight dip % higher, P/E lower"
|
||
|
||
5. **Backtest shows signal accuracy** — User moves from gut to data
|
||
- "My thesis was right 18% of time, wrong 82%"
|
||
- "Strong Buy + Dip is 67% accurate, Hold is 48% accurate"
|
||
- "Which of MY filters actually predict returns?"
|
||
|
||
### 10.5h — Design Language (Professional, Minimalist)
|
||
|
||
- **Palette:** Black text, white surfaces, semantic colors (green success, red danger, amber warning)
|
||
- **Typography:** Monospace for numbers (Arial Mono, 11px), sans-serif for labels (Anthropic Sans, 12px)
|
||
- **Spacing:** 12px gaps, 16px padding (tight, professional)
|
||
- **No decoration:** Flat design, 0.5px borders only, no shadows/gradients/icons (icons only in Tabler outline)
|
||
- **Dark mode native:** All colors use CSS variables (--color-text-primary, --color-background-secondary, etc.)
|
||
- **Sticky elements:** Header always visible, sidebar sticky on desktop
|
||
|
||
### 10.5i — Mobile Responsiveness
|
||
|
||
- **Desktop (>1200px):** Sidebar | Table | Tearsheet (3-column layout)
|
||
- **Tablet (768-1200px):** Sidebar collapses to icon panel | Table (full width) | Tearsheet overlays
|
||
- **Mobile (<768px):** Sidebar in drawer | Table scrolls horizontally | Tearsheet = full-screen modal
|
||
|
||
### 10.5j — Comprehensive Free Data Stack (Zero Cost, Zero Redundancy)
|
||
|
||
---
|
||
|
||
## Phase 10.6 — Portfolio Integration: Market Analysis → Action
|
||
|
||
**Goal:** Connect screener signals + market context to portfolio decisions. Guide users through complete workflow: Find stock → Understand market backdrop → Size position → Track thesis → Measure outcome.
|
||
|
||
### 10.6a — Market-Aware Position Sizing
|
||
|
||
Calculate recommended position size (not user's job):
|
||
- Stock verdict (Strong Buy = larger, Hold = smaller)
|
||
- Market regime (High rates = trim growth, favor value)
|
||
- Sector momentum (Hot = reduce exposure, cold = add exposure)
|
||
- Portfolio allocation (already 22% tech = cap new tech at 2%)
|
||
|
||
Display: "Recommended: 2-4% of your portfolio"
|
||
Show dollar amount: "If you have $100k, buy $2,000-$4,000"
|
||
|
||
### 10.6b — Portfolio Dashboard: Integrated View
|
||
|
||
Single screen shows all three:
|
||
1. **Holdings:** Current positions + P&L
|
||
2. **Allocation vs Target:** Visual breakdown (overweight/underweight)
|
||
3. **Market Context:** Fed, VIX, sector trends
|
||
4. **Screener Signals:** How many Strong Buys/Holds/Avoids you own
|
||
5. **Recommended Action:** What to do (trim/add/rebalance/do nothing)
|
||
|
||
### 10.6c — Screener-Portfolio Bridge
|
||
|
||
In screener table, add column: "Your Holdings"
|
||
- "You own 2% | +$1,000 gain"
|
||
- "Verdict changed from Strong Buy → Hold (consider trimming)"
|
||
- Alert: "Your thesis on this changed (verdict downgraded)"
|
||
|
||
### 10.6d — Thesis Journal (Simplified)
|
||
|
||
When adding position to portfolio:
|
||
1. Why I'm buying (pick ONE reason)
|
||
2. What I'll watch (pick 1-2 metrics)
|
||
3. Review date (auto 30 days)
|
||
|
||
After 30 days: "Did your thesis hold?"
|
||
- Stock price: Up/Down/Flat
|
||
- Analyst consensus: Upgraded/Same/Downgraded
|
||
- Thesis status: Still valid / Partially broken / Completely wrong
|
||
|
||
Track accuracy over time (learning loop).
|
||
|
||
### 10.6e — Rebalancing Advisor
|
||
|
||
Monitor allocation vs target.
|
||
When screener verdict changes on existing holding:
|
||
- Alert user
|
||
- Show impact on portfolio
|
||
- Suggest specific action (trim this, add that)
|
||
|
||
When market context shifts (Fed decision, rate change):
|
||
- Recalculate position attractiveness
|
||
- Recommend sector rotation (take profits in expensive growth, add cheap value)
|
||
|
||
---
|
||
|
||
## Phase 10.7 — Newbie UX: Progressive Disclosure
|
||
|
||
**Goal:** Professional tool with newbie-friendly interface. Same power, different experience. Default simple, reveal detail on demand. Plain language always.
|
||
|
||
**Core principle:** Don't simplify the tool. Simplify the interface.
|
||
|
||
### 10.7a — Screener Entry: Strategy-Based (Not Filter-Based)
|
||
|
||
Instead of showing filters (P/E range, ROE min, dip %, etc):
|
||
|
||
Ask: "What are you looking for?"
|
||
|
||
```
|
||
Options:
|
||
○ Solid companies at good prices (Balanced)
|
||
→ Auto-applies: Quality gate PASS + Reasonable valuation
|
||
|
||
○ Hot stocks with momentum (Momentum)
|
||
→ Auto-applies: Positive 52W momentum + analyst upgrades
|
||
|
||
○ Beaten-down bargains (Value)
|
||
→ Auto-applies: Low P/E + High dividend yield
|
||
|
||
○ Let me customize filters (Advanced)
|
||
→ Shows full filter panel for pros
|
||
```
|
||
|
||
**Why it works:**
|
||
- Newbies pick a *strategy*, not filters
|
||
- Tool auto-applies appropriate gates
|
||
- Advanced users still have access to full customization
|
||
- Feels like guidance, not overwhelming data
|
||
|
||
### 10.7b — Table View: Plain Language Explanations
|
||
|
||
Minimal table (Ticker | Price | Verdict | Why? ℹ️)
|
||
|
||
Clicking "ℹ️" shows plain-language explanation:
|
||
|
||
```
|
||
WHY IS AAPL A STRONG BUY?
|
||
|
||
3 Key Reasons:
|
||
|
||
1️⃣ GREAT COMPANY
|
||
Apple makes money really well.
|
||
Profit margins: 46% (excellent)
|
||
Debt: Manageable
|
||
Cash: Strong
|
||
|
||
💡 What this means: You're buying a healthy business
|
||
Quality Score: 95/100
|
||
|
||
2️⃣ REASONABLE PRICE
|
||
Cost: 28.5x yearly earnings
|
||
Peers: 18x-32x
|
||
Conclusion: Fair price (not cheap, not expensive)
|
||
|
||
💡 What this means: You're not overpaying
|
||
Value Score: 85/100
|
||
|
||
3️⃣ GOOD TIMING
|
||
Price: 5% below 52-week high
|
||
Analysts: Think it'll go up 12%
|
||
Market: Fed paused rate hikes (good for stocks)
|
||
|
||
💡 What this means: Now is an OK time to buy
|
||
Timing Score: 80/100
|
||
|
||
═══════════════════════════════════════
|
||
VERDICT: Strong Buy ✓
|
||
Confidence: 8/10 (Pretty confident)
|
||
|
||
[Learn More] [Add to Portfolio] [Not Ready]
|
||
```
|
||
|
||
**Features:**
|
||
- Plain language (no jargon)
|
||
- Icons + visual hierarchy
|
||
- "What this means" translation
|
||
- Clear CTA buttons
|
||
|
||
### 10.7c — Buy Decision Helper
|
||
|
||
When user considers adding a stock:
|
||
|
||
```
|
||
YOU'RE CONSIDERING: TSLA - $241.85
|
||
|
||
QUICK ASSESSMENT:
|
||
┌─────────────────────────────┐
|
||
│ IS THIS A GOOD BUY? │
|
||
│ Company Quality: ★★★★★ │
|
||
│ Price Level: ★★★☆☆ │
|
||
│ Timing: ★★★★☆ │
|
||
│ OVERALL: ★★★★☆ │
|
||
└─────────────────────────────┘
|
||
|
||
💡 4 stars = Recommended
|
||
You'll probably make money
|
||
|
||
HOW MUCH SHOULD YOU BUY?
|
||
|
||
Based on your portfolio:
|
||
Recommended: 2-4% of your money
|
||
|
||
If you have $100,000:
|
||
Buy $2,000-$4,000 worth
|
||
= About 9-18 shares
|
||
|
||
Why 2-4%?
|
||
Don't put too much in ONE stock.
|
||
Spread money across many = lower risk.
|
||
|
||
YOUR DECISION:
|
||
|
||
✓ [Add 2%] - Safe (recommended)
|
||
✓ [Add 3%] - Moderate confidence
|
||
✓ [Add 4%] - High conviction
|
||
✗ [More than 4%] - Too risky, not recommended
|
||
○ [Skip, Wait for Dip] - Maybe later at better price
|
||
```
|
||
|
||
**Why it works:**
|
||
- Star rating is intuitive
|
||
- Position size auto-calculated (not user's job)
|
||
- Concrete dollars (not abstract %)
|
||
- Clear "safe" path highlighted
|
||
- Option to wait shown upfront
|
||
|
||
### 10.7d — Portfolio Status View (Not Analysis)
|
||
|
||
Instead of complex metrics, show status + guidance:
|
||
|
||
```
|
||
YOUR PORTFOLIO STATUS: ✓ HEALTHY
|
||
|
||
YOUR BREAKDOWN:
|
||
┌──────────────────────────────────┐
|
||
│ Tech ████████████░░░░░░ 22% │
|
||
│ Target: 20% (2% over) │
|
||
│ │
|
||
│ Healthcare ██████░░░░░░░░░░░ 18% │
|
||
│ Target: 15% (3% over) │
|
||
│ │
|
||
│ Other ███████████░░░░░░░░ 60% │
|
||
│ Target: 65% (5% under) │
|
||
└──────────────────────────────────┘
|
||
|
||
💡 WHAT THIS MEANS:
|
||
You own too much Tech & Healthcare
|
||
Not enough in Everything Else
|
||
This is FINE (not dangerous) but...
|
||
|
||
⚠️ WHAT COULD HAPPEN:
|
||
If Tech drops 10% → Your portfolio drops 8%
|
||
If Everything Else drops 10% → You feel 6%
|
||
= You're slightly overexposed to tech risk
|
||
|
||
✅ WHAT TO DO (Pick one):
|
||
|
||
Option A: TAKE PROFITS (Beginner-friendly)
|
||
Sell 2% of tech stocks
|
||
You lock in gains + reduce risk
|
||
Takes: 5 minutes
|
||
[Do This] ← Recommended
|
||
|
||
Option B: BUY OTHER SECTORS (Intermediate)
|
||
Add money to Healthcare & Other
|
||
Diversify without selling winners
|
||
Takes: 30 minutes
|
||
[Learn How]
|
||
|
||
Option C: DO NOTHING (Advanced)
|
||
Your allocation is acceptable
|
||
Monitor quarterly
|
||
[I'm Comfortable]
|
||
```
|
||
|
||
**Why it works:**
|
||
- Visual (bars are immediately clear)
|
||
- Explains impact (concrete numbers)
|
||
- Gives multiple options (not prescriptive)
|
||
- Recommends safest for beginners
|
||
- Supports different experience levels
|
||
|
||
### 10.7e — Market Context: Status Light + Impact
|
||
|
||
Instead of raw data, use traffic light system:
|
||
|
||
```
|
||
🟢 MARKET HEALTH: Good
|
||
The market is calm. Stocks are fairly priced.
|
||
Good time to invest (not too risky).
|
||
|
||
⚠️ WHAT TO KNOW: Interest rates are high
|
||
Banks benefit (lending is expensive)
|
||
Growth stocks suffer (future profits worth less)
|
||
|
||
Impact on YOUR portfolio:
|
||
• Your tech stocks: Slightly risky 📉
|
||
• Your bank stocks: Doing well 📈
|
||
|
||
💡 WHAT TO DO:
|
||
Nothing urgent, but...
|
||
If adding money: Prefer banks & stable companies
|
||
If rebalancing: Trim expensive tech, add value stocks
|
||
```
|
||
|
||
**Three layers:**
|
||
1. Status indicator (🟢🟡🔴)
|
||
2. Plain explanation (why market is here)
|
||
3. Impact on their portfolio + action
|
||
|
||
### 10.7f — Thesis Logging: Simple Checklist
|
||
|
||
Not open-ended journal. Simple form:
|
||
|
||
```
|
||
YOU BOUGHT TSLA: $241.85 (4% of portfolio)
|
||
|
||
WHY ARE YOU BUYING?
|
||
(Pick ONE reason):
|
||
|
||
○ Strong growth company (earnings growing fast)
|
||
○ Undervalued (price low vs business)
|
||
○ Sector tailwind (industry heating up)
|
||
○ I just liked it (no specific reason)
|
||
|
||
[I chose: "Strong growth company"]
|
||
|
||
WHAT WILL YOU WATCH?
|
||
(Pick 1-2 metrics):
|
||
|
||
☐ Stock price (is it going up?)
|
||
☐ Earnings (beating estimates?)
|
||
☐ Analyst ratings (still bullish?)
|
||
☐ Sector news (industry still growing?)
|
||
|
||
[I'll watch: Stock price, Analyst ratings]
|
||
|
||
═══════════════════════════════════
|
||
IN 30 DAYS, WE'LL CHECK:
|
||
✓ Did stock price go up?
|
||
✓ Any analyst downgrades?
|
||
✓ Still bullish on sector?
|
||
|
||
You'll learn: "Did my pick work?"
|
||
```
|
||
|
||
**Why it works:**
|
||
- Simple checklist (not overwhelming)
|
||
- Focuses on 1-2 metrics (not 10+)
|
||
- Built-in review schedule (30-day check-in)
|
||
- Learning reinforced without lecturing
|
||
|
||
### 10.7g — After Buying: 30-Day Check-In
|
||
|
||
System auto-reminds user after 30 days:
|
||
|
||
```
|
||
HOW DID YOUR TSLA PICK WORK?
|
||
|
||
Your thesis: "Strong growth company"
|
||
|
||
CHECK YOUR METRICS:
|
||
|
||
Stock Price:
|
||
Entry: $241.85
|
||
Now: $255.00
|
||
Change: +5.3% ✓ (Good!)
|
||
|
||
Your thesis depends on: Stock going UP
|
||
Status: On track ✓
|
||
|
||
Analyst Ratings:
|
||
Were: 75% Buy
|
||
Now: 78% Buy
|
||
Change: Upgraded ✓
|
||
|
||
Your thesis depends on: Bullish consensus
|
||
Status: Intact ✓
|
||
|
||
═══════════════════════════════════
|
||
RESULT: Your thesis is WORKING ✓
|
||
|
||
2/2 metrics on track.
|
||
Keep holding OR take some profits.
|
||
|
||
[Keep Holding] [Take 20% Profit] [Exit All]
|
||
```
|
||
|
||
**Why it works:**
|
||
- Automated reminder (user doesn't forget)
|
||
- Shows actual vs predicted
|
||
- Clear thesis validation
|
||
- Suggests next action
|
||
- Builds learning habit
|
||
|
||
### 10.7h — Newbie Mode vs Pro Mode (Toggle)
|
||
|
||
**Newbie Mode (default):**
|
||
- Simplified screener entry (strategy-based)
|
||
- Plain language explanations
|
||
- Auto position sizing
|
||
- Status lights (not data)
|
||
- Guided workflows
|
||
|
||
**Pro Mode (toggle in settings):**
|
||
- Full filter control
|
||
- All metrics visible
|
||
- Raw data + charts
|
||
- Advanced analysis
|
||
- Complete transparency
|
||
|
||
Same tool, two interfaces.
|
||
|
||
User can toggle anytime:
|
||
```
|
||
Settings > Experience Level
|
||
○ Newbie (Simplified, guided)
|
||
○ Intermediate (Mixed)
|
||
○ Pro (Full control, no guardrails)
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 10.8 — Earnings Calendar: Context, Not Destination
|
||
|
||
**Strategic Principle:** Calendar data should be integrated contextually into decision workflows, NOT a standalone navigation tab.
|
||
|
||
**Why NOT a Calendar Tab:**
|
||
|
||
❌ **Low discoverability:** Users browse screener, find stock, *then* want to know when it reports. They don't open a separate calendar tab.
|
||
|
||
❌ **Out-of-context data:** Earnings dates alone = reference data. Earnings dates WITH screener verdict + thesis = actionable intelligence.
|
||
|
||
❌ **Navigation friction:** Breaks user flow. Instead of stock → tearsheet, forces stock → calendar tab → search → check.
|
||
|
||
❌ **Redundant:** Same data appears in 3 places (screener, portfolio, calendar), creating maintenance debt.
|
||
|
||
❌ **Wrong mental model:** Calendar is reference. Your platform is decision-focused.
|
||
|
||
**Better Approach: Earnings as Decision Context**
|
||
|
||
#### **10.8a — Earnings in Screener Tearsheet (Primary)**
|
||
|
||
When user clicks stock, earnings section shows:
|
||
|
||
```
|
||
UPCOMING EVENTS:
|
||
├── Earnings: July 30, 2026 (18 days away)
|
||
│ ├── EPS estimate: $6.50
|
||
│ ├── Historical beat rate: 65% (beats estimate 65% of time)
|
||
│ ├── Avg price move on earnings: +3% (beat), -2% (miss)
|
||
│ └── Timing decision: "Buy now before earnings?" or "Wait to see results?"
|
||
│
|
||
├── Ex-dividend: June 15 (6 days away)
|
||
│ └── Dividend: $0.24/share
|
||
│
|
||
└── Analyst call: Post-earnings July 30
|
||
└── Action: "Watch call for forward guidance"
|
||
```
|
||
|
||
**Why here:**
|
||
- Stock + earnings together (context matters)
|
||
- Timing-based decision ("Should I buy before or after earnings?")
|
||
- Thesis-aware ("If earnings beat, thesis validates")
|
||
- Newbie-friendly ("What does earnings mean for my decision?")
|
||
|
||
**Implementation:**
|
||
- Fetch from Finnhub (earnings, estimates, surprise data)
|
||
- Calculate historical beat rate (from historical data)
|
||
- Show avg price move (from options implied volatility or historical moves)
|
||
- Provide decision guidance ("Reduce position before surprise risk" vs "Hold through catalyst")
|
||
|
||
#### **10.8b — Earnings in Portfolio (Secondary)**
|
||
|
||
Portfolio holdings view shows upcoming events for YOUR positions:
|
||
|
||
```
|
||
YOUR HOLDINGS - UPCOMING CATALYSTS:
|
||
├── AAPL: Earnings July 30 (18 days) | Your position: 2% | Verdict: Hold
|
||
│ ✓ Your thesis: iPhone growth → Watch if guidance raised
|
||
│ ✓ Decision: Consider adding if earnings beat + guidance raised
|
||
│ ⚠ Risk: If revenue guidance disappoints, -5-10% likely
|
||
│
|
||
├── MSFT: Earnings July 24 (12 days) | Your position: 3% | Verdict: Buy
|
||
│ ✓ Your thesis: Cloud growth → Watch cloud revenue % of total
|
||
│ ✓ Decision: Set buy limit if beaten down after earnings
|
||
│ ⚠ Risk: If azure growth slows <25% YoY, re-evaluate
|
||
│
|
||
└── NVDA: Earnings August 3 (28 days) | Your position: 2% | Verdict: Hold (at peak)
|
||
⚠ Your thesis: AI cycle resilience → At risk from guidance miss
|
||
✓ Decision: Consider taking 50% profit BEFORE earnings (reduce risk)
|
||
✓ Reason: Stock at peak, earnings miss would be -15-20%
|
||
```
|
||
|
||
**Why here:**
|
||
- Shows when YOUR holdings report (not just all earnings)
|
||
- Thesis-specific tracking ("What earnings data validates/breaks your thesis?")
|
||
- Risk management ("Reduce position before surprise events")
|
||
- Decision-oriented ("What should you do given upcoming earnings?")
|
||
|
||
**Implementation:**
|
||
- Overlay screener verdicts on portfolio holdings
|
||
- Show decision guidance based on thesis + verdict combo
|
||
- Risk warnings for stocks at peaks or with weak guidance expectations
|
||
- Reminders to take profits before uncertain catalysts
|
||
|
||
#### **10.8c — Earnings Discovery Widget (Optional, Tertiary)**
|
||
|
||
Optional light calendar feature in screener header (NOT main nav):
|
||
|
||
```
|
||
SCREENER HEADER:
|
||
Filtered: 247 | Strong Buy: 12 | Buy: 34 | Hold: 56
|
||
|
||
📅 25 earnings this week in your screened results
|
||
└── [View by day] [View by verdict]
|
||
```
|
||
|
||
Clicking shows:
|
||
|
||
```
|
||
EARNINGS IN YOUR SCREENED STOCKS:
|
||
|
||
This Week:
|
||
├── Monday 6/9: 5 reporting
|
||
│ ├── TSLA (Strong Buy) - earnings 4:30pm ET
|
||
│ ├── NFLX (Buy) - earnings 5pm ET
|
||
│ └── 3 others
|
||
│
|
||
├── Tuesday 6/10: 8 reporting
|
||
│ ├── NVDA (Hold) - earnings 5pm ET
|
||
│ └── 7 others
|
||
│
|
||
└── Wednesday 6/11: 12 reporting
|
||
```
|
||
|
||
**Why here:**
|
||
- Proactive discovery ("Which stocks I'm looking at report soon?")
|
||
- Not competing with main nav (sub-feature in header)
|
||
- Contextual to screener (only shows stocks user is actually screening)
|
||
- Decision support ("Should I wait to see earnings before buying?")
|
||
|
||
**Implementation:**
|
||
- Light modal/dropdown (not full page)
|
||
- Filter by verdict (show "Strong Buys reporting this week")
|
||
- Link back to stock tearsheet (click to see full earnings context)
|
||
|
||
#### **10.8d — What NOT to Build**
|
||
|
||
❌ **Standalone "Calendar" nav tab**
|
||
- Creates navigation bloat
|
||
- Out-of-context data (just dates, no decisions)
|
||
- Low usage (users don't proactively browse earnings separate from stocks)
|
||
- Redundant (data already in screener + portfolio)
|
||
|
||
#### **10.8e — Earnings Calendar in Thesis Journal**
|
||
|
||
When user logs a thesis, earnings become a key tracking metric:
|
||
|
||
```
|
||
THESIS JOURNAL:
|
||
You bought: AAPL @ $189.50
|
||
|
||
Why: "iPhone 18 launch cycle"
|
||
|
||
Key metrics to track:
|
||
☑ Stock price (investor signal)
|
||
☑ Analyst ratings (consensus shift)
|
||
☑ Earnings (thesis validation)
|
||
└── Next earnings: July 30 (18 days)
|
||
└── What to watch: Revenue guidance + iPhone % of revenue
|
||
└── What breaks thesis: If iPhone <40% of revenue (market share loss)
|
||
|
||
AUTOMATED TRACKING:
|
||
30-day reminder: Did stock move in line with your thesis?
|
||
→ Earnings will occur on July 30
|
||
→ Check: Did earnings beat/miss? Did guidance hold?
|
||
→ Update: Thesis status (intact / shaken / broken)
|
||
```
|
||
|
||
**Why this works:**
|
||
- Earnings as thesis validation (not just date reference)
|
||
- Newbie learning ("This is why earnings matter for my pick")
|
||
- Decision trigger ("If earnings break my thesis, I exit")
|
||
|
||
---
|
||
|
||
## Strategic Summary: Earnings Calendar Architecture (TABULAR)
|
||
|
||
| Component | Where | Why | Cost |
|
||
|---|---|---|---|
|
||
| **Earnings dates + estimates** | Screener tearsheet | Context for timing decisions | Low (Finnhub API) |
|
||
| **Earnings + thesis tracking** | Portfolio view | Manage existing positions | Low (already integrated) |
|
||
| **Historical beat rate** | Screener tearsheet | Estimate surprise probability | Low (historical data) |
|
||
| **Earnings in thesis journal** | Decision logging | Validate thesis over time | Low (metadata tracking) |
|
||
| **Earnings discovery widget** | Screener header badge | Proactive browsing (nice-to-have) | Medium (optional) |
|
||
| **Standalone calendar tab** | DONT BUILD | Navigation clutter, low value | N/A |
|
||
|
||
**Key Principle:** Calendar is context for decisions, not a destination.
|
||
|
||
### 10.8f — Design Note: Revisit Earnings Display Format
|
||
|
||
**⚠️ DESIGN REVIEW NEEDED:**
|
||
|
||
Current plan shows earnings integrated across three locations (tearsheet, portfolio, journal). Consider:
|
||
|
||
1. **Information consistency:** Does earnings data show the same way in all three places, or should each context adapt it?
|
||
- Screener tearsheet: "Earnings July 30 (18 days). Beat prob: 65%"
|
||
- Portfolio: "AAPL earnings July 30. Your thesis: iPhone growth. Risk: Guidance miss"
|
||
- Journal: "Earnings is key metric #3 to track for outcome"
|
||
|
||
2. **Visual hierarchy:** How prominent should earnings be vs. other data?
|
||
- Screener: Part of "Upcoming Events" section (secondary)
|
||
- Portfolio: Inline with risk management (primary)
|
||
- Journal: One of 3-4 tracked metrics (equal weight)
|
||
|
||
3. **Mobile responsiveness:** Earnings data may not fit on small screens.
|
||
- Desktop: Full table with all fields
|
||
- Mobile: Abbreviated ("July 30" not "July 30, 2026 - 4:30 PM ET")
|
||
|
||
**Recommendation:** Implement Phase 10.5 + 10.6, then review actual user behavior before finalizing visual design. Adjust based on what users actually click on and spend time looking at.
|
||
|
||
---
|
||
|
||
## Phase 10.9 — Strong Buys: Professional Dip Opportunity Monitor
|
||
|
||
**Goal:** Flag quality stocks ("too big to fail") when they drop 5%+ from 52W high, with market analysis of why they dipped.
|
||
|
||
**Use case:** "AAPL dropped 5% today. Was it company fundamentals or macro headwind? If macro, it's a dip to buy."
|
||
|
||
**NOT a newbie feature. This is professional dip-buying opportunity detection.**
|
||
|
||
### 10.9a — Strong Buys Data Structure
|
||
|
||
| Field | Source | Purpose |
|
||
|-------|--------|---------|
|
||
| Ticker | Yahoo Finance | Stock identifier |
|
||
| Current Price | Yahoo Finance (real-time daily fetch) | Entry price today |
|
||
| 52W High | Yahoo Finance | Reference point for dip % |
|
||
| Dip % | Calculated: (high - current) / high | Triggers display if ≥5% |
|
||
| Screener Verdict | ScreenerEngine | Is it Strong Buy or just cheap? |
|
||
| Screener Score | ScreenerEngine | Quality ranking (8.2/10 vs 5.5/10) |
|
||
| Dip Reason | Market Context Analysis | Macro (Fed), sector rotation, catalyst, or company issue |
|
||
| Market Context | Daily fetched (Fed, sector trends, catalysts) | Why did it drop? Is it temporary? |
|
||
| Your Play | LLM analysis | What should you do? Buy the dip or wait? |
|
||
| Recommended Action | Position sizing logic | "Add 2-4% to portfolio" |
|
||
|
||
### 10.9b — Fetching Mechanism (Daily Update)
|
||
|
||
```
|
||
Daily Job (EOD or Morning):
|
||
|
||
Step 1: Get "Too Big to Fail" Stock Universe
|
||
├── Tier 1: Mega-cap only (10 stocks)
|
||
│ └── AAPL, MSFT, NVDA, GOOG, AMZN, TSLA, META, BERKB, etc
|
||
├── Tier 2: Large-cap (50 stocks)
|
||
│ └── >$10B market cap, curated quality
|
||
├── Tier 3: User's custom watchlist
|
||
│ └── User-added stocks to monitor
|
||
└── Total: ~150 stocks to screen
|
||
|
||
Step 2: Fetch Prices + 52W High (One Yahoo batch call)
|
||
└── Returns: current price, 52W high, volume, etc.
|
||
|
||
Step 3: Filter Dips ≥5% from 52W High
|
||
└── Calculate: (52W high - current price) / 52W high
|
||
└── Keep only: dip % ≥ 5%
|
||
└── Example: AAPL 52W=$210, today=$189 → 9.76% ✓
|
||
|
||
Step 4: Run Screener on Dipped Stocks (One screener call)
|
||
└── ScreenerEngine.screenTickers(dipped_tickers)
|
||
└── Returns: verdict, score, market analysis
|
||
|
||
Step 5: Analyze Why Dipped (Use cached market context)
|
||
├── Macro factor (Fed held rates → growth punished)
|
||
├── Sector rotation (Capital flowing from A to B)
|
||
├── Catalyst (Ex-dividend, earnings coming)
|
||
└── Company issue (Earnings miss, guidance cut)
|
||
|
||
Step 6: Combine + Cache (TTL 24 hours)
|
||
└── Store as: { ticker, price, dip%, verdict, reason, thesis, action }
|
||
└── Next update: Tomorrow same time
|
||
|
||
Step 7: API Serves from Cache (Zero latency)
|
||
└── GET /api/strong-buys → returns cached results
|
||
```
|
||
|
||
### 10.9c — UI: Tabular Display of Dip Opportunities
|
||
|
||
| Ticker | Price | Dip % | Verdict | Why It Dipped | Your Play | Action |
|
||
|--------|-------|-------|---------|---------------|-----------|--------|
|
||
| AAPL | $189.50 | -9.76% | Strong Buy (8.2) | Fed rates high (macro headwind, not company issue) | Buy the dip. iPhone growth intact. Temporary. | [+2-4%] |
|
||
| JPM | $215.30 | -7.2% | Strong Buy (7.8) | Sector rotation (capital → tech from banks) | Defensive play. Banks undervalued. Patient entry. | [+3%] |
|
||
| MSFT | $425.30 | -3.1% | Buy (7.1) | Minor pullback (no major catalyst) | Not yet dip enough. Watch for 5%+. | [Skip] |
|
||
|
||
**Table Features:**
|
||
- Sortable by: Dip %, Verdict, Your Play
|
||
- Click row → full tearsheet with thesis + market analysis
|
||
- Daily refresh: Updates each morning/EOD
|
||
- Dip threshold configurable: 5% (default) → 10% → 15%
|
||
|
||
### 10.9d — Configuration (User Control)
|
||
|
||
```
|
||
Settings > Strong Buys Monitor:
|
||
|
||
Stock Universe:
|
||
☑ Mega-cap (10 stocks)
|
||
☑ Large-cap (50 stocks)
|
||
☑ My Watchlist (custom)
|
||
|
||
Dip Threshold:
|
||
○ 5% (Aggressive - most opportunities)
|
||
○ 10% (Balanced)
|
||
○ 15% (Conservative - only major dips)
|
||
|
||
Update Frequency:
|
||
○ Daily morning (9:30 AM)
|
||
● Daily EOD (4:00 PM)
|
||
○ 4x daily (more frequent updates - future)
|
||
|
||
Alerts (Future):
|
||
☐ Email when dip detected
|
||
☐ Discord webhook
|
||
☐ Push notification
|
||
```
|
||
|
||
### 10.9e — Design Note: Revisit Tabular Format
|
||
|
||
**⚠️ DESIGN REVIEW NEEDED:**
|
||
|
||
The tabular format above is **functional but may be dense** for quick scanning. Consider:
|
||
|
||
1. **Card-based alternative** (cleaner, easier to scan):
|
||
```
|
||
┌──────────────────────────────────┐
|
||
│ AAPL | $189.50 | -9.76% │
|
||
│ Strong Buy (8.2/10) │
|
||
├──────────────────────────────────┤
|
||
│ WHY: Fed rates high (macro) │
|
||
│ NOT: Company fundamentals │
|
||
├──────────────────────────────────┤
|
||
│ PLAY: Buy the dip │
|
||
│ Add: 2-4% to portfolio │
|
||
│ [Add to Portfolio] [Analyze] │
|
||
└──────────────────────────────────┘
|
||
```
|
||
|
||
2. **Compact table** (current proposal):
|
||
- Pro: All info visible at once
|
||
- Con: Wide table, may require horizontal scroll on mobile
|
||
|
||
3. **Hybrid approach** (desktop table + mobile cards):
|
||
- Desktop: Full table
|
||
- Mobile: Card view
|
||
|
||
**Recommendation:** Review after Phase 10.9a implementation. Gather user feedback on what works better.
|
||
|
||
---
|
||
|
||
## Phase 10.8 — Earnings Calendar: Context, Not Destination (TABULAR)
|
||
|
||
**Philosophy:** Build professional-grade screener using only FREE sources. Layer specialized APIs intelligently — no bloated $99-$200/mo subscriptions. Each source has ONE clear job (no duplication).
|
||
|
||
**Architecture Principle:**
|
||
- **Yahoo Finance (via YahooFinanceClient):** Stock metrics only (what you already have)
|
||
- **yfinance:** Per-ticker enrichment only (news, earnings dates)
|
||
- **Finnhub FREE:** Earnings calendar + estimates only
|
||
- **Alpha Vantage FREE:** Market context only (macro trends)
|
||
- **API Ninjas FREE:** Earnings backup only (redundancy layer)
|
||
- **Your LLM:** Intelligence layer (turns data into insights)
|
||
|
||
**The Stack (All Free, No Redundancy):**
|
||
|
||
1. **Yahoo Finance (via YahooFinanceClient) — METRICS ONLY**
|
||
- Core metrics: P/E, ROE, FCF, D/E, analyst ratings, market cap, 52W high/low
|
||
- Insider activity, institutional holdings
|
||
- Already integrated in your ScreenerEngine
|
||
- **Role:** Calculate screener scores + verdicts
|
||
- **NOT used for:** News (use yfinance instead), earnings calendar (use Finnhub instead)
|
||
|
||
2. **yfinance Library — ENRICHMENT ONLY**
|
||
- Per-ticker news articles (top 5-10)
|
||
- Earnings dates (historical + future)
|
||
- Dividend history, options chain
|
||
- **Role:** Fetch stock-specific news for tearsheet
|
||
- **NOT used for:** Fundamental metrics (already have from Yahoo)
|
||
- **Why yfinance not Yahoo direct:** Optimized for news extraction, cleaner interface
|
||
|
||
3. **Finnhub FREE Tier — EARNINGS CALENDAR + ESTIMATES**
|
||
- Upcoming earnings calendar (3-month lookahead)
|
||
- EPS consensus estimates
|
||
- Earnings surprise data (actual vs. estimated)
|
||
- **Role:** Reliable earnings dates + estimates for decision triggers
|
||
- **NOT used for:** Stock prices or fundamentals (have from Yahoo)
|
||
- **Why Finnhub:** More reliable for future events than Yahoo
|
||
|
||
4. **Alpha Vantage FREE Tier — MARKET CONTEXT + SENTIMENT**
|
||
- Daily market news headlines (macro-focused)
|
||
- AI sentiment analysis (positive/neutral/negative)
|
||
- Ticker mention extraction (which stocks in headlines)
|
||
- Keyword search (Fed decisions, economic data, sector trends)
|
||
- **Role:** Provide market backdrop + macro sentiment for screener header
|
||
- **NOT used for:** Stock-specific data (use yfinance instead)
|
||
- **Why separate:** Completely different data type (macro, not micro)
|
||
|
||
5. **API Ninjas FREE Tier — EARNINGS BACKUP**
|
||
- Upcoming earnings dates (100 requests/month free)
|
||
- Filter by exchange, date range, ticker
|
||
- **Role:** Redundancy layer (if Finnhub API hits rate limits)
|
||
- **NOT used for:** Primary source (Finnhub is primary)
|
||
- **Why backup:** Ensures reliability, handles spikes
|
||
|
||
6. **Your LLM (Claude) — INTELLIGENCE LAYER**
|
||
- News sentiment analysis (beyond Alpha Vantage baseline)
|
||
- Decision framework generation ("which gates pass/fail, why?")
|
||
- Risk narrative scoring (quantify uncertainties)
|
||
- Thesis validation (does news confirm your thesis?)
|
||
- **Role:** Turn raw data into actionable insights
|
||
- **NOT used for:** Data fetching (all data from external sources)
|
||
|
||
**Data Flow in Tearsheet:**
|
||
|
||
```
|
||
Step 1: User screens stocks
|
||
→ ScreenerEngine uses YahooFinanceClient (your existing code)
|
||
→ Returns: 247 stocks with scores, verdicts, metrics
|
||
|
||
Step 2: Metrics cached in memory/state
|
||
→ No additional API calls needed
|
||
→ Display compact table instantly
|
||
|
||
Step 3: User clicks row → Tearsheet opens
|
||
→ All metrics already loaded (from Step 1)
|
||
|
||
Step 4: Fetch per-ticker enrichment (on-demand, parallel)
|
||
→ yfinance.Ticker(ticker).news → top 5 articles
|
||
→ Finnhub earnings/{ticker} → next earnings + estimates
|
||
→ Alpha Vantage called once daily (cached) for market context
|
||
|
||
Step 5: Process with LLM (if enabled)
|
||
→ Analyze yfinance news → sentiment
|
||
→ Validate thesis against news + fundamentals
|
||
→ Generate decision framework
|
||
|
||
Step 6: Display complete tearsheet
|
||
├── Core Metrics (Yahoo, cached from screener)
|
||
├── Valuation Context (Yahoo peer comparison)
|
||
├── Decision Framework (gates pass/fail, from ScoringConfig)
|
||
├── Recent News (yfinance + LLM sentiment)
|
||
├── Upcoming Events (Finnhub earnings + estimates)
|
||
├── Market Context (Alpha Vantage + sentiment)
|
||
├── Risk Breakdown (LLM analysis)
|
||
├── Peer Comparison (Yahoo data)
|
||
└── CTA (Decision Log, Analyze)
|
||
```
|
||
|
||
**Integration Timeline (Phased):**
|
||
|
||
**Phase 1 (Week 1): Add yfinance News Enrichment**
|
||
- Create endpoint: `GET /api/screen/:ticker/news`
|
||
- Returns top 5 yfinance articles for ticker
|
||
- Display in tearsheet "Recent News" section
|
||
- Cost: 0 (yfinance free)
|
||
|
||
**Phase 2 (Week 2): Add Finnhub Earnings Calendar**
|
||
- Create endpoint: `GET /api/screen/:ticker/earnings`
|
||
- Returns next earnings date + EPS estimates + surprise data
|
||
- Display in tearsheet "Upcoming Events" section
|
||
- Cost: 0 (Finnhub FREE tier, rate limit: 60 req/min, plenty for your volume)
|
||
|
||
**Phase 3 (Week 3): Add Alpha Vantage Market Context**
|
||
- Create endpoint: `GET /api/market-context`
|
||
- Called once daily (cached), returns top market news + sentiment
|
||
- Display in screener header "Market Context" strip
|
||
- LLM optional: analyze headlines → sector impact
|
||
- Cost: 0 (Alpha Vantage FREE tier)
|
||
|
||
**Phase 4 (Week 4): Add API Ninjas as Backup**
|
||
- Create endpoint: `GET /api/earnings-calendar`
|
||
- Returns earnings calendar for portfolio
|
||
- Used as fallback if Finnhub hits rate limits
|
||
- Cost: 0 (API Ninjas FREE tier, 100 req/month)
|
||
|
||
**Phase 5 (Week 5): Wire Everything into Tearsheet**
|
||
- Compile all data sources into single modal view
|
||
- Optimize API calls (parallel, cached where appropriate)
|
||
- Test rate limits under load
|
||
|
||
**Phase 6 (Week 6): Add LLM Enrichment**
|
||
- Optional: Send yfinance news + fundamentals → LLM
|
||
- Generate sentiment analysis
|
||
- Generate decision framework ("why this verdict?")
|
||
- Add to tearsheet
|
||
|
||
**Why This Approach:**
|
||
|
||
✅ **Zero Cost:** $0/month (all sources FREE)
|
||
✅ **Zero Redundancy:** Each source has ONE job, no overlap
|
||
✅ **Professional Grade:** Layered sources like institutional traders use
|
||
✅ **Reliability:** Redundancy where it matters (earnings calendar via Finnhub + backup via API Ninjas)
|
||
✅ **Intelligent:** Your LLM adds 10x value without additional data cost
|
||
✅ **Teachable:** Users see full decision framework, learn what matters
|
||
|
||
**Cost Comparison:**
|
||
|
||
| Stack | Monthly Cost | Quality | Breadth | Redundancy |
|
||
|-------|---|---|---|---|
|
||
| Zacks API | $200+ | High | Limited | No |
|
||
| Finnhub PRO | $99 | High | Excellent | No |
|
||
| Your FREE stack | **$0** | **HIGH** | **EXCELLENT** | **Strategic (earnings only)** |
|
||
|
||
**Rate Limits & Sustainability:**
|
||
|
||
- Yahoo Finance (via YahooFinanceClient): No official limits (already proven in production)
|
||
- yfinance: No limits (wraps Yahoo, same as above)
|
||
- Finnhub FREE: 60 API calls/minute (sufficient for screening 250 stocks, polling earnings calendar)
|
||
- Alpha Vantage FREE: 5 calls/minute (one daily call to market news, easily manageable)
|
||
- API Ninjas: 100 calls/month (backup only, minimal usage)
|
||
|
||
**Scaling Plan:**
|
||
|
||
- **Users 1-1000:** Current FREE stack, no cost
|
||
- **Users 1000-10,000:** Upgrade to Finnhub FREE tier (still free, same rate limit works)
|
||
- **Users 10,000+:** Upgrade to Finnhub PAID tier ($99/mo) OR use API Ninjas as primary with Finnhub as backup
|
||
- **Enterprise (100K+):** Evaluate FactSet/Bloomberg (only if paying customers justify cost)
|
||
|
||
**Key Insight:**
|
||
|
||
You don't need one expensive, comprehensive API. You need **five free, specialized sources** composed intelligently. This is exactly how professional traders and quants build systems:
|
||
|
||
1. **Yahoo (YahooFinanceClient)** = broad fundamentals (your screening foundation)
|
||
2. **yfinance** = stock-specific catalysts (is this stock moving for a reason?)
|
||
3. **Finnhub** = earnings calendar (what's the catalyst timeline?)
|
||
4. **Alpha Vantage** = market sentiment (is the tide rising or falling?)
|
||
5. **API Ninjas** = backup reliability (always have a Plan B)
|
||
6. **Your LLM** = intelligence layer (what does all this mean for my decision?)
|
||
|
||
Together, they're better than any single $200/mo API because they're specialized, not bloated. This is professional-grade without the professional price tag.
|
||
|
||
---
|
||
|
||
## Phase 11 — Day Trading: Authentication & Authorization
|
||
|
||
**Goal:** Add multi-user support with JWT auth, role-based access control, and user portfolio isolation.
|
||
|
||
**Timeline:** 2-3 weeks. Do this BEFORE adding any real-time trading features (Phase 12+).
|
||
|
||
### Why Auth is First
|
||
|
||
Without auth, you **cannot test**:
|
||
- Multi-user portfolios (can't separate user A's holdings from user B's)
|
||
- Public + private access (can't invite members)
|
||
- Discord notifications with user context (can't tag which user triggered it)
|
||
- Trade journal with user attribution (can't track who made the decision)
|
||
|
||
### 11a — Create auth domain
|
||
|
||
```
|
||
server/domains/auth/
|
||
├── AuthController.ts (POST /auth/login, /auth/register, /auth/refresh)
|
||
├── AuthService.ts (JWT generation, password hashing)
|
||
├── JWTStrategy.ts (JWT validation + extraction)
|
||
├── RBACGuard.ts (middleware for role checks)
|
||
├── persistence/
|
||
│ └── UserStore.ts (users table CRUD)
|
||
└── types/
|
||
├── auth.model.ts (User, Token, Role types)
|
||
└── schemas.ts (JSON schemas for /auth/* requests)
|
||
```
|
||
|
||
### 11b — Database schema changes
|
||
|
||
Add to SQLite:
|
||
|
||
```sql
|
||
-- users table
|
||
CREATE TABLE users (
|
||
id TEXT PRIMARY KEY,
|
||
email TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
role TEXT DEFAULT 'viewer' CHECK (role IN ('trader', 'viewer', 'admin')),
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
last_login DATETIME
|
||
);
|
||
|
||
-- Extend holdings table with user_id
|
||
ALTER TABLE holdings ADD COLUMN user_id TEXT NOT NULL REFERENCES users(id);
|
||
|
||
-- Extend portfolio_advice with user context
|
||
ALTER TABLE portfolio_advice ADD COLUMN user_id TEXT REFERENCES users(id);
|
||
|
||
-- Extend market_calls with creator tracking
|
||
ALTER TABLE market_calls ADD COLUMN created_by TEXT REFERENCES users(id);
|
||
```
|
||
|
||
### 11c — Middleware + route protection
|
||
|
||
Update `server/app.ts`:
|
||
|
||
```typescript
|
||
app.register(require('@fastify/jwt'), {
|
||
secret: process.env.JWT_SECRET || 'dev-secret-change-me',
|
||
});
|
||
|
||
// Apply RBACGuard to protected routes
|
||
app.post('/api/portfolio/add',
|
||
{ onRequest: [authGuard, roleGuard('trader')] },
|
||
portfolioController.add
|
||
);
|
||
|
||
app.get('/api/trading/safe-buys',
|
||
{ onRequest: [authGuard, roleGuard('trader')] },
|
||
tradingController.safeBuys
|
||
);
|
||
```
|
||
|
||
### 11d — UI auth layer
|
||
|
||
Add to SvelteKit:
|
||
|
||
```
|
||
routes/
|
||
└── auth/
|
||
├── login/
|
||
│ ├── +page.ts
|
||
│ └── +page.svelte
|
||
└── register/
|
||
├── +page.ts
|
||
└── +page.svelte
|
||
|
||
lib/stores/
|
||
└── auth.store.svelte.ts (currentUser, JWT, login/logout)
|
||
|
||
lib/api/
|
||
└── auth.ts (login, register, refresh endpoints)
|
||
```
|
||
|
||
**Commit:** `feat: add Phase 11 — authentication & RBAC`
|
||
|
||
---
|
||
|
||
## Phase 12 — Day Trading: News Webhooks
|
||
|
||
**Goal:** Ingest real-time market news via Polygon.io webhooks and trigger downstream analysis.
|
||
|
||
**Timeline:** 2-3 weeks.
|
||
|
||
### Why Webhooks Come Second
|
||
|
||
News feeds everything downstream:
|
||
- Safe Buys monitor watches for tickers mentioned in news
|
||
- LLM analysis needs fresh news context
|
||
- Price dips are more valuable when correlated with news
|
||
|
||
### 12a — Create news domain
|
||
|
||
```
|
||
server/domains/news/
|
||
├── NewsController.ts (POST /webhooks/news for Polygon)
|
||
├── WebhookHandler.ts (parse + validate Polygon events)
|
||
├── NewsStore.ts (insert articles + search)
|
||
├── NewsQueue.ts (BullMQ worker for async processing)
|
||
├── persistence/
|
||
│ └── NewsArticleStore.ts (news_articles table)
|
||
└── types/
|
||
└── news.model.ts (Article, PolygonEvent types)
|
||
```
|
||
|
||
### 12b — Database schema
|
||
|
||
```sql
|
||
CREATE TABLE news_articles (
|
||
id TEXT PRIMARY KEY,
|
||
ticker TEXT NOT NULL,
|
||
headline TEXT NOT NULL,
|
||
body TEXT,
|
||
source TEXT,
|
||
url TEXT,
|
||
sentiment TEXT, -- positive, neutral, negative (optional)
|
||
published_at DATETIME,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY(ticker) REFERENCES holdings(ticker)
|
||
);
|
||
|
||
CREATE INDEX idx_news_ticker_date ON news_articles(ticker, published_at DESC);
|
||
CREATE INDEX idx_news_created ON news_articles(created_at DESC);
|
||
```
|
||
|
||
### 12c — Set up Polygon.io webhook
|
||
|
||
1. Subscribe to Polygon news API (requires paid tier, ~$200/month)
|
||
2. Register webhook endpoint: `https://yourapp.com/webhooks/news`
|
||
3. Validate webhook signature (Polygon sends HMAC)
|
||
4. Queue article for processing (don't block HTTP response)
|
||
|
||
```typescript
|
||
// NewsController.ts
|
||
async handleWebhook(req: FastifyRequest, reply: FastifyReply) {
|
||
const signature = req.headers['x-polygon-signature'];
|
||
if (!validateSignature(req.body, signature)) {
|
||
return reply.code(401).send({ error: 'Invalid signature' });
|
||
}
|
||
|
||
// Queue immediately, respond fast
|
||
await newsQueue.add('ingest', req.body);
|
||
return reply.code(202).send({ status: 'queued' });
|
||
}
|
||
```
|
||
|
||
### 12d — Async processing with BullMQ
|
||
|
||
```typescript
|
||
// NewsQueue.ts
|
||
newsQueue.process('ingest', async (job) => {
|
||
const article = job.data;
|
||
|
||
// 1. Store in DB
|
||
await newsStore.insert(article);
|
||
|
||
// 2. Trigger LLM analysis if article mentions key tickers
|
||
const mentionedTickers = extractTickers(article.body);
|
||
for (const ticker of mentionedTickers) {
|
||
await llmQueue.add('analyze', { ticker, article });
|
||
}
|
||
|
||
// 3. Notify subscribers (Discord, etc)
|
||
await notifySubscribers(article);
|
||
|
||
return { status: 'processed' };
|
||
});
|
||
```
|
||
|
||
**Commit:** `feat: add Phase 12 — news webhooks & async processing`
|
||
|
||
---
|
||
|
||
## Phase 13 — Day Trading: Prompt Caching & LLM Optimization
|
||
|
||
**Goal:** Reduce LLM costs by 90% using Anthropic prompt caching. Store analysis results in DB for fast retrieval.
|
||
|
||
**Timeline:** 2-3 weeks.
|
||
|
||
### 13a — Create llm domain (refactored)
|
||
|
||
```
|
||
server/domains/llm/
|
||
├── LLMRouter.ts (NEW: route by cost/model)
|
||
├── PromptCache.ts (NEW: Anthropic cache mgmt)
|
||
├── LLMAnalyst.ts (refactored from shared)
|
||
├── persistence/
|
||
│ ├── AnalysisStore.ts (llm_analysis table)
|
||
│ └── CacheStore.ts (prompt_cache table)
|
||
└── types/
|
||
└── llm.model.ts (Analysis, CacheEntry types)
|
||
```
|
||
|
||
### 13b — Database schema
|
||
|
||
```sql
|
||
CREATE TABLE llm_analysis (
|
||
id TEXT PRIMARY KEY,
|
||
ticker TEXT NOT NULL,
|
||
analysis_result TEXT NOT NULL, -- JSON: signal, sentiment, risks
|
||
model_used TEXT DEFAULT 'claude-opus',
|
||
tokens_used INTEGER,
|
||
cache_hit BOOLEAN DEFAULT false,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
expires_at DATETIME -- for caching strategy
|
||
);
|
||
|
||
CREATE TABLE prompt_cache (
|
||
cache_key TEXT PRIMARY KEY,
|
||
prompt_hash TEXT NOT NULL,
|
||
result TEXT NOT NULL,
|
||
model TEXT,
|
||
expires_at DATETIME,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
|
||
CREATE INDEX idx_analysis_ticker ON llm_analysis(ticker);
|
||
CREATE INDEX idx_analysis_expires ON llm_analysis(expires_at);
|
||
```
|
||
|
||
### 13c — Implement Anthropic prompt caching
|
||
|
||
```typescript
|
||
// PromptCache.ts
|
||
async analyze(ticker: string, newsContext: string): Promise<Analysis> {
|
||
const systemPrompt = buildSystemPrompt(); // ~5000 tokens, static
|
||
const userPrompt = buildUserPrompt(ticker, newsContext);
|
||
|
||
const response = await anthropic.messages.create({
|
||
model: 'claude-opus-4-1',
|
||
max_tokens: 1000,
|
||
system: [
|
||
{
|
||
type: 'text',
|
||
text: systemPrompt,
|
||
cache_control: { type: 'ephemeral' }, // Cache this forever
|
||
},
|
||
],
|
||
messages: [
|
||
{
|
||
role: 'user',
|
||
content: userPrompt,
|
||
},
|
||
],
|
||
});
|
||
|
||
// Log cache metrics
|
||
const { usage } = response;
|
||
await analysisStore.insert({
|
||
ticker,
|
||
result: parseJSON(response.content[0].text),
|
||
model: 'claude-opus-4-1',
|
||
cache_hit: usage.cache_read_input_tokens > 0,
|
||
tokens_used: usage.input_tokens + usage.output_tokens,
|
||
});
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
### 13d — LLM Router for cost optimization
|
||
|
||
```typescript
|
||
// LLMRouter.ts
|
||
async analyze(ticker: string): Promise<Analysis> {
|
||
const isCostSensitive = true; // set based on usage/quota
|
||
|
||
const model = isCostSensitive
|
||
? 'claude-sonnet' // cheaper, 90% quality
|
||
: 'claude-opus'; // best quality
|
||
|
||
try {
|
||
return await llmAnalyst.analyze(ticker, model);
|
||
} catch (error) {
|
||
if (error.status === 429) { // rate limited
|
||
// Fallback to OpenAI GPT-4 Turbo
|
||
return await openaiAnalyst.analyze(ticker);
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Commit:** `feat: add Phase 13 — prompt caching & LLM optimization`
|
||
|
||
---
|
||
|
||
## Phase 14 — Day Trading: Safe Buys Monitor with Discord Alerts
|
||
|
||
**Goal:** Monitor safe-buy stocks in real-time, detect 5%+ dips, and notify via Discord.
|
||
|
||
**Timeline:** 3-4 weeks.
|
||
|
||
### 14a — Create trading domain
|
||
|
||
```
|
||
server/domains/trading/
|
||
├── TradingController.ts (GET /api/trading/safe-buys)
|
||
├── DipDetector.ts (5% threshold logic)
|
||
├── PriceMonitor.ts (Alpaca/IB price polling)
|
||
├── DiscordNotifier.ts (webhook to Discord)
|
||
├── persistence/
|
||
│ ├── PriceSnapshotStore.ts (price snapshots)
|
||
│ └── TradeSignalStore.ts (buy/sell signals)
|
||
└── types/
|
||
└── trading.model.ts (Signal, Dip, Alert types)
|
||
```
|
||
|
||
### 14b — Database schema
|
||
|
||
```sql
|
||
CREATE TABLE price_snapshots (
|
||
id TEXT PRIMARY KEY,
|
||
ticker TEXT NOT NULL,
|
||
price REAL NOT NULL,
|
||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
source TEXT, -- 'alpaca', 'interactive_brokers', 'polygon'
|
||
dip_detected BOOLEAN DEFAULT false
|
||
);
|
||
|
||
CREATE TABLE trading_signals (
|
||
id TEXT PRIMARY KEY,
|
||
ticker TEXT NOT NULL,
|
||
signal_type TEXT CHECK (signal_type IN ('strong_buy', 'dip', 'warning')),
|
||
entry_price REAL,
|
||
detected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
notified BOOLEAN DEFAULT false,
|
||
outcome TEXT -- 'win', 'loss', 'pending' (after 5 days)
|
||
);
|
||
|
||
CREATE INDEX idx_price_ticker_time ON price_snapshots(ticker, timestamp DESC);
|
||
CREATE INDEX idx_signal_notified ON trading_signals(notified, ticker);
|
||
```
|
||
|
||
### 14c — Real-time price polling
|
||
|
||
```typescript
|
||
// PriceMonitor.ts (runs every 5 seconds)
|
||
async checkPrices() {
|
||
const watchedTickers = await getWatchedTickers(); // from holdings
|
||
|
||
for (const ticker of watchedTickers) {
|
||
const currentPrice = await alpacaAdapter.getPrice(ticker);
|
||
const previousPrice = await priceSnapshotStore.getLatest(ticker);
|
||
|
||
const priceChange = ((currentPrice - previousPrice) / previousPrice) * 100;
|
||
|
||
// Store snapshot
|
||
await priceSnapshotStore.insert({ ticker, price: currentPrice });
|
||
|
||
// Check for 5% dip
|
||
if (priceChange <= -5) {
|
||
await dipDetector.processDip({
|
||
ticker,
|
||
entry_price: previousPrice,
|
||
current_price: currentPrice,
|
||
pct_change: priceChange,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 14d — Discord notifications
|
||
|
||
```typescript
|
||
// DiscordNotifier.ts
|
||
async notifyDip(alert: DipAlert) {
|
||
const llmAnalysis = await llmAnalyst.analyze(alert.ticker);
|
||
|
||
const embed = {
|
||
title: `🔴 5% Dip Detected: ${alert.ticker}`,
|
||
description: `Price fell from $${alert.entry_price} to $${alert.current_price} (${alert.pct_change.toFixed(2)}%)`,
|
||
fields: [
|
||
{ name: 'LLM Analysis', value: llmAnalysis.sentiment },
|
||
{ name: 'Recommendation', value: llmAnalysis.signal },
|
||
{ name: 'Risks', value: llmAnalysis.risks.join(', ') },
|
||
],
|
||
color: 0xff0000, // red
|
||
};
|
||
|
||
await fetch(process.env.DISCORD_WEBHOOK_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ embeds: [embed] }),
|
||
});
|
||
}
|
||
```
|
||
|
||
### 14e — UI: Safe Buys Monitor
|
||
|
||
Add to SvelteKit:
|
||
|
||
```
|
||
routes/
|
||
└── trading/
|
||
└── safe-buys/
|
||
├── +page.ts
|
||
└── +page.svelte (TickerWatchList, DipAlerts components)
|
||
```
|
||
|
||
**Commit:** `feat: add Phase 14 — real-time safe buys monitor`
|
||
|
||
---
|
||
|
||
## Phase 15 — Day Trading: Trade Journal & Performance Tracking
|
||
|
||
**Goal:** Log every decision, track outcomes, and measure strategy performance over time.
|
||
|
||
**Timeline:** 1-2 weeks.
|
||
|
||
### 15a — Database schema
|
||
|
||
```sql
|
||
CREATE TABLE trade_journal (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL REFERENCES users(id),
|
||
ticker TEXT NOT NULL,
|
||
signal TEXT, -- 'strong_buy', 'momentum', 'dip', etc
|
||
entry_price REAL NOT NULL,
|
||
entry_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
exit_price REAL,
|
||
exit_date DATETIME,
|
||
outcome TEXT CHECK (outcome IN ('win', 'loss', 'pending')),
|
||
pnl REAL, -- profit/loss in dollars
|
||
reason TEXT, -- why you took the trade
|
||
notes TEXT
|
||
);
|
||
|
||
CREATE INDEX idx_journal_user ON trade_journal(user_id, entry_date DESC);
|
||
CREATE INDEX idx_journal_outcome ON trade_journal(outcome);
|
||
```
|
||
|
||
### 15b — Trade stats dashboard
|
||
|
||
Compute daily aggregates:
|
||
|
||
```typescript
|
||
// TradeJournal service
|
||
async getDailyStats(userId: string) {
|
||
const trades = await db.query(`
|
||
SELECT * FROM trade_journal
|
||
WHERE user_id = ? AND DATE(entry_date) = DATE('now')
|
||
`, [userId]);
|
||
|
||
return {
|
||
total_trades: trades.length,
|
||
winning_trades: trades.filter(t => t.outcome === 'win').length,
|
||
losing_trades: trades.filter(t => t.outcome === 'loss').length,
|
||
win_rate: ...,
|
||
total_pnl: trades.reduce((sum, t) => sum + (t.pnl || 0), 0),
|
||
avg_win: ...,
|
||
avg_loss: ...,
|
||
};
|
||
}
|
||
```
|
||
|
||
### 15c — UI: Trade Stats Dashboard
|
||
|
||
```
|
||
routes/
|
||
└── trading/
|
||
└── journal/
|
||
├── +page.ts
|
||
└── +page.svelte (TradeStats, TradeHistory components)
|
||
```
|
||
|
||
**Commit:** `feat: add Phase 15 — trade journal & performance tracking`
|
||
|
||
---
|
||
|
||
## Phase 16 — Multi-LLM Support (Optional, Weeks 8-9)
|
||
|
||
**Goal:** Support Claude, OpenAI, and optionally Llama for cost optimization and experimentation.
|
||
|
||
**Timeline:** 2-3 weeks (do after Phase 14 core monitor works).
|
||
|
||
### Minimal implementation:
|
||
|
||
```typescript
|
||
// LLMRouter.ts
|
||
const MODELS = {
|
||
'claude-opus': { cost: 0.015, speed: 'slow', quality: 'best' },
|
||
'claude-sonnet': { cost: 0.003, speed: 'fast', quality: 'good' },
|
||
'gpt-4': { cost: 0.03, speed: 'medium', quality: 'excellent' },
|
||
'gpt-3.5-turbo': { cost: 0.002, speed: 'fast', quality: 'ok' },
|
||
};
|
||
|
||
async analyze(ticker: string, preferredModel?: string) {
|
||
const model = preferredModel || 'claude-sonnet'; // default: cheap + good
|
||
return await {
|
||
'claude-opus': anthropicAnalyst,
|
||
'claude-sonnet': anthropicAnalyst,
|
||
'gpt-4': openaiAnalyst,
|
||
}[model].analyze(ticker);
|
||
}
|
||
```
|
||
|
||
**Commit:** `feat: add Phase 16 — multi-LLM routing`
|
||
|
||
---
|
||
|
||
## Final Architecture Summary
|
||
|
||
After Phases 11-16, your app:
|
||
|
||
| Layer | Tech | Status |
|
||
|-------|------|--------|
|
||
| **Auth** | JWT + RBAC | ✅ Weeks 1-2 |
|
||
| **Data** | SQLite (→ Postgres if 1000+ users) | ✅ Phase 11 |
|
||
| **News** | Polygon.io webhooks | ✅ Phase 12 |
|
||
| **LLM** | Anthropic + OpenAI w/ prompt caching | ✅ Phase 13-14 |
|
||
| **Trading** | Real-time price monitoring + Discord alerts | ✅ Phase 14 |
|
||
| **Tracking** | Trade journal + performance stats | ✅ Phase 15 |
|
||
| **UI** | Svelte 5 + Phase 10 structure | ✅ Phase 10 |
|
||
|
||
**Cost per month:** ~$330-450 (Polygon + APIs + infrastructure)
|
||
**Codebase size:** ~3500 LOC server + 1500 LOC UI (clean, navigable)
|
||
**UI latency:** <100ms (async queue + caching)
|
||
**Time to ship:** 12-16 weeks solo, 8 weeks with 1-2 junior devs
|
||
|
||
---
|
||
|
||
## Adding a New Asset Type
|
||
|
||
1. Create a subclass of `Asset` in `server/models/` with a flat `metrics` object and `getDisplayMetrics()`.
|
||
2. Add a per-type entry (`gates` / `weights` / `thresholds`) to `ScoringRules` in `server/config/ScoringConfig.ts`.
|
||
3. Add inflated overrides in `server/services/MarketRegime.ts` → `getInflatedOverrides()`.
|
||
4. Create a Scorer in `server/scorers/` exposing `score(metrics, rules, marketContext)`.
|
||
5. Add a mapper branch in `server/services/DataMapper.ts`.
|
||
6. Wire into `server/services/ScreenerEngine.ts`: add `case` in `_buildAsset`, entry in `SCORERS` map.
|
||
7. Add the new type to `serializeAssets()` in `server/controllers/screener.controller.ts`.
|
||
|
||
---
|
||
|
||
## Clean Architecture Pattern (Server-Side)
|
||
|
||
### Core Principles
|
||
|
||
1. **Types in `server/types/`** — Domain shapes only; one `*.model.ts` per domain
|
||
2. **Schemas in `server/types/schemas.ts`** — Fastify JSON Schema objects for request body validation
|
||
3. **Class-Based Implementation** — All `.ts` files (except types/schemas) are classes
|
||
4. **No Inline Interfaces** — All types in `server/types/`; private-only shapes may stay inline
|
||
5. **Module-Level Cleanliness** — No module-level constants or functions outside classes
|
||
6. **Direct Imports** — Import directly from source files; use `server/types/index.ts` barrel for types
|
||
7. **Service barrel** — `server/services/index.ts` re-exports all services so controllers import from one path
|
||
|
||
### Implementation Pattern
|
||
|
||
#### Controllers (Classes with DI)
|
||
```typescript
|
||
export class ScreenerController {
|
||
constructor(private readonly engine: ScreenerEngine) {}
|
||
|
||
register(app: FastifyInstance): void {
|
||
app.post('/api/screen', { schema: screenSchema }, this.screen.bind(this));
|
||
}
|
||
|
||
private async screen(req: FastifyRequest) {
|
||
const tickers = (req.body as { tickers: string[] }).tickers.map(t => t.toUpperCase());
|
||
return this.engine.screenTickers(tickers);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Services (Instance methods, static helpers)
|
||
```typescript
|
||
export class BenchmarkProvider {
|
||
private static readonly TTL_MS = 60 * 60 * 1000;
|
||
|
||
async getMarketContext(): Promise<MarketContext> { ... }
|
||
|
||
private static rateRegime(rate: number): string {
|
||
return rate < 2 ? 'LOW' : rate <= 5 ? 'NORMAL' : 'HIGH';
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Scorers (Static-only classes)
|
||
```typescript
|
||
export class StockScorer {
|
||
static score(metrics: StockMetrics, rules: ScoringRules): ScoreResult { ... }
|
||
private static _sanitize(m: StockMetrics): SanitizedMetrics { ... }
|
||
}
|
||
```
|
||
|
||
### Import Guidelines
|
||
|
||
```typescript
|
||
// ✅ CORRECT
|
||
import { ScreenerEngine } from '../services/ScreenerEngine'; // direct for implementations
|
||
import { ScreenerEngine } from '../services'; // via barrel also acceptable
|
||
import type { StockMetrics } from '../types'; // always use types barrel
|
||
import { screenSchema } from '../types/schemas'; // direct for schemas
|
||
|
||
// ❌ WRONG
|
||
import type { StockMetrics } from '../types/models.model'; // use barrel instead
|
||
```
|
||
|
||
### Adding a New API Endpoint
|
||
|
||
1. Define types → `server/domains/shared/types/<domain>.model.ts`
|
||
2. Define schema → `server/domains/shared/schemas.ts`
|
||
3. Create service → `server/domains/<domain>/<Service>.ts`
|
||
4. Wire controller → `server/domains/<domain>/<Domain>Controller.ts`
|
||
5. Register → `server/app.ts`
|
||
|
||
---
|
||
|
||
## Day Trading: Cost Estimation & Production Readiness
|
||
|
||
### Monthly Operating Costs (Steady State)
|
||
|
||
| Service | Cost | Notes |
|
||
|---------|------|-------|
|
||
| Polygon.io (real-time news + quotes) | $200 | Entry tier, required for webhooks |
|
||
| Anthropic Claude API (w/ prompt caching) | $50–100 | Most analyses cached; reduces cost 90% |
|
||
| OpenAI API (fallback, optional) | $50 | Only if you add GPT-4 as fallback |
|
||
| Alpaca/Interactive Brokers (real-time data) | $30–100 | Depends on which feed you choose |
|
||
| BullMQ (Redis queue, if scaled) | $0–30 | Free if self-hosted; $30/mo if managed |
|
||
| **Total** | **~$330–450/month** | Scales well (no per-user seat cost) |
|
||
|
||
### Cost Optimization Strategies
|
||
|
||
1. **Prompt Caching (Phase 13)** — Saves $50–100/month by reusing cached system prompts
|
||
2. **Batch LLM Calls** — Process 10 articles in 1 call instead of 10 calls
|
||
3. **Smart Polling** — Check watched tickers every 5s, others every 60s
|
||
4. **Cache Price Data** — Redis or in-memory cache for frequently accessed tickers
|
||
|
||
### Production Checklist (Before Going Live)
|
||
|
||
- [ ] Environment variables locked down (.env.production, no secrets in code)
|
||
- [ ] Database: Migrate from SQLite to Postgres if expect >10 concurrent users
|
||
- [ ] Job Queue: Set up BullMQ with Redis (or Bull's memory adapter for small scale)
|
||
- [ ] Logging: Add structured logging (Winston, Pino) to track LLM calls + costs
|
||
- [ ] Rate Limiting: Enabled on all public endpoints (@fastify/rate-limit)
|
||
- [ ] Discord Webhook: Test alerts with real market data
|
||
- [ ] Auth: JWT secret rotated, session timeout set to 1h
|
||
- [ ] SSL/TLS: HTTPS enforced, domain SSL cert in place
|
||
- [ ] Monitoring: Set up alerts for:
|
||
- Job queue backlog (if >100 pending, page on-call)
|
||
- API latency (target <100ms for UI, <5s for LLM)
|
||
- Prompt cache hit rate (should be >80%)
|
||
- Webhook failure rate (should be <0.1%)
|
||
- Alpaca price feed staleness (should be <5s)
|
||
|
||
### Postgres Migration Path (When Needed)
|
||
|
||
If you grow to 10+ active traders:
|
||
|
||
1. Create Postgres RDS instance (AWS: ~$15/mo, db.t3.micro)
|
||
2. Update connection string in `.env` to point to Postgres
|
||
3. Run schema dump from SQLite → Postgres (pg_restore)
|
||
4. Test thoroughly on staging first
|
||
5. Blue-green deploy: run both DBs in parallel for 1 day, switch, keep SQLite as backup
|
||
6. Update backup strategy: use pg_dump + S3, not JSON files
|
||
|
||
Time: 2–4 hours. No code changes needed (uses same `better-sqlite3` interface wrapper).
|
||
|
||
### Monitoring Dashboard (Recommended)
|
||
|
||
Once you're live, track:
|
||
- Daily active traders
|
||
- Average win rate (target: >55% for trending tickers)
|
||
- Prompt cache efficiency (% cache hits)
|
||
- Webhook latency (p50, p95, p99)
|
||
- LLM cost per analysis
|
||
- System uptime (target: 99.5%)
|
||
|
||
Use Grafana + Prometheus, or simple JSON endpoint that logs to CloudWatch.
|
||
|
||
---
|
||
|
||
## Frequently Asked Questions
|
||
|
||
### Q: How many traders can this system handle?
|
||
|
||
**A:**
|
||
- **10–50 traders:** Single instance (current setup). Costs ~$450/mo.
|
||
- **50–500 traders:** Add Postgres + Redis queue. Costs ~$1000/mo.
|
||
- **500+ traders:** Add Kubernetes cluster + load balancing. Costs ~$5000+/mo.
|
||
|
||
For now, optimize for the first tier. Scaling is a good problem to have.
|
||
|
||
### Q: What if Polygon.io goes down?
|
||
|
||
**A:** Have a fallback plan:
|
||
1. Switch to Finnhub webhooks (similar API, different provider)
|
||
2. Or fall back to polling (5s instead of real-time, less expensive)
|
||
3. Add circuit breaker: if Polygon fails for >5 min, automatically switch to polling
|
||
|
||
### Q: Can I trade with real money using this?
|
||
|
||
**A:** Yes, but:
|
||
1. Start with **paper trading** (Alpaca's paper account, no real money)
|
||
2. Test for 2+ weeks on real market conditions
|
||
3. Once you hit 55%+ win rate on paper, go live with small position sizes
|
||
4. Scale up gradually (1% of portfolio → 5% → 10%)
|
||
5. Always have a manual kill-switch (can disable alerts + halt new trades)
|
||
|
||
### Q: Should I use local LLM training?
|
||
|
||
**A:** Not yet. Only consider if:
|
||
- You have 6+ months of clean trade data
|
||
- Your LLM bill is >$1000/mo (suggests high volume)
|
||
- You have $20K+ to spend on GPU infrastructure + ops
|
||
|
||
For now, optimize prompts instead. A good prompt beats a fine-tuned model.
|
||
|
||
---
|
||
|
||
## Roadmap Summary
|
||
|
||
| Phase | Feature | Effort | When |
|
||
|-------|---------|--------|------|
|
||
| 9 | Server refactor (domains) | 3 weeks | June 2026 |
|
||
| 10 | UI refactor (components) | 1 week | June 2026 |
|
||
| 11 | Auth & RBAC | 2-3 weeks | Early July 2026 |
|
||
| 12 | News webhooks | 2-3 weeks | Mid July 2026 |
|
||
| 13 | Prompt caching | 2-3 weeks | Late July (parallel) |
|
||
| 14 | Safe buys monitor | 3-4 weeks | August 2026 |
|
||
| 15 | Trade journal | 1-2 weeks | Late August 2026 |
|
||
| 16 | Multi-LLM router | 2-3 weeks | September 2026 (optional) |
|
||
| 17 | Local LLM training | 6-8 weeks | 2027+ (maybe) |
|
||
|
||
**Total time to "trading ready":** 12-16 weeks solo, 8 weeks with 1 junior dev.
|
||
**Go-live target:** Q3 2026 (July–September).
|
||
**Current status:** Phase 9-10 prep work (this month), then Phase 11 (auth) next month. You're at Q2 2026, so you have the full summer to ship.
|