Files
equitysearch/openspec/changes/stock-deep-evaluation/design.md
T
paulandClaude Opus 4.8 902758ce67 Build stock-deep-evaluation v1: single-stock equity evaluation app
Next.js 15 + TypeScript app implementing the fully-specced first change.
Pipeline: resolve -> market data -> pure evaluation engine -> budget guard
-> analysis agent -> report.

- market-data: DataProvider interface, offline FixtureProvider (DE/SPY seeded
  from the reference example), FmpProvider (FMP free tier), TTL cache + retry.
- technicals: pure MA/volatility/swing/52-week math.
- evaluation: instrument-aware pure engine; equity branch built, ETF gated to
  "not yet supported". Reproduces the DE example (P/E 34.5, fwd 29.3, $167.6B).
- agent: AnalysisAgent interface; default Claude Code CLI transport (headless,
  subscription-backed, web-grounded), Anthropic API alternate via config.
- cost-controls: price table, spend store, monthly budget guard.
- UI: ticker search + deep-dive toggle, report view, price chart with marked
  entry/exit/stop levels, cost/budget display, ETF/not-found states.

31 vitest tests, typecheck, production build, and lint all pass. Verified
end-to-end via the API for DE, SPY, and an unknown ticker. Live Claude CLI
agent test is the documented pick-up point (see README).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-21 17:03:04 -04:00

11 KiB

Context

This is a greenfield Next.js + TypeScript web app that turns a single NYSE/Nasdaq ticker into a thorough, agent-written evaluation (valuation, macro, entry/exit, stop-loss). The target output shape is captured concretely in examples/de-reentry-2026-08-21.md and encoded as requirements in the four spec files. Constraints: bring-your-own API keys, free-tier market data by default, rate limits, and no persistence in v1. The design must keep data-provider and model-provider choices swappable so cost/quality decisions don't ripple into the evaluation or UI code.

Goals / Non-Goals

Goals:

  • One typed pipeline: resolve ticker → fetch data → compute evaluation → agent narrative.
  • Provider-agnostic boundaries: DataProvider and AnalysisAgent interfaces are the only seams consumers see.
  • Deterministic, testable evaluation math separated from the non-deterministic agent prose. The structured evaluation must stand on its own even with no agent key.
  • Secrets stay server-side only.

Non-Goals:

  • Persistence, accounts, watchlists, alerts, real-time streaming, backtesting.
  • Multi-ticker dashboards. (Deferred to later changes.)
  • Trade execution or brokerage integration.

Decisions

D1 — App structure: Next.js App Router, server-side data/agent calls

Route handlers (or server actions) under app/api/* run the pipeline server-side; the client is a thin report view. Rationale: keeps API keys off the browser (satisfies the app-config spec), and lets data + agent calls share one request. Alternative considered: client-side fetching — rejected because it would leak keys and duplicate rate-limit handling.

D2 — DataProvider interface with a free default

A single interface returns normalized models: CompanyProfile, Fundamentals (TTM + per-FY, optional segments), PriceHistory, TechnicalContext (computed), AnalystCoverage, Estimates. Default implementation targets a free tier (candidate: Financial Modeling Prep or Finnhub; final pick during tasks). Each model carries an asOf timestamp and delayed|realtime flag. Alternative: code directly against one vendor — rejected for lock-in and testability.

D3 — Technical indicators computed in-house, not taken from the provider

Moving averages (20/50/200), swing highs/lows, 52-week distance, volume multiple, and realized volatility are computed from OHLCV in a pure technicals module. Rationale: providers expose these inconsistently; in-house math is deterministic and unit-testable, and it is the same input the entry/exit/stop logic needs.

D4 — Evaluation engine is pure and provider-free

evaluate(inputs) → Evaluation is a pure function over normalized data. It emits a typed Evaluation object (one field per spec section) with explicit unavailable markers — never fabricated or zero-filled values. All entry/exit/ stop levels, implied multiples at each level, and valuation reasoning are computed here. Rationale: the spec requires the structured object to be consumable without the agent, and pure functions make the many numeric requirements testable.

D5 — AnalysisAgent interface; Claude Code CLI default, API alternate

The agent takes the Evaluation object and returns narrative sections. Consumers depend only on the interface, so the transport is swappable. Default transport: the local Claude Code CLI in headless modeclaude -p --output-format json, running on the operator's existing Claude auth (subscription), so an internal two-person tool incurs ~no metered cost. Alternate transport: the Anthropic API via @anthropic-ai/sdk, selected by config, for when the app is served to external users (subscription-backed serving is not appropriate then). Default model is Opus 4.8 (CLI alias opus); a per-request deep-dive flag escalates to Fable 5 (CLI alias fable) — model is a per-request parameter, not a build-time constant. Structured narrative is requested via the CLI --json-schema option (or the API alternate's structured-output). The agent is instructed to use the supplied figures plus attributed web-sourced facts, preserve flagged caveats, and include the disclaimer. When no agent is available the pipeline returns the Evaluation with narrative: unavailable.

CLI invocation shape (verified against claude --help): claude -p "<prompt>" --model <opus|fable> --output-format json --json-schema '<narrative schema>' --append-system-prompt "<instructions>" --allowedTools "WebSearch WebFetch" --permission-mode dontAsk. The JSON result carries the structured output plus total_cost_usd and token usage (consumed by cost-controls). API-alternate notes (from the SDK reference): Opus 4.8 and Fable 5 take thinking: {type: "adaptive"}, reject budget_tokens/sampling params; Fable 5 has thinking always-on and can return stop_reason: "refusal" (enable a server-side fallback to Opus 4.8); stream long outputs.

D8 — Live web grounding via the agent's web tools

The agent grounds macro/news facts through its web search + fetch capability — Claude Code's WebSearch/WebFetch tools on the default transport, or Anthropic's server-side web tools on the API alternate — scoped to the ticker, with sources attributed in the output. Rationale: the reference DE evaluation's credibility came from live, cited facts (analyst target changes, tariff figures, peer read-throughs, catalyst dates) that don't exist in model weights or a fundamentals feed — this is the layer that closes the gap to a search-augmented tool like Perplexity. A bonus of the CLI default: this grounding harness is built in, so there is nothing to wire up. Grounding degrades gracefully: on tool error the agent still writes from the structured evaluation and notes that live grounding was unavailable.

D6 — Typed results end-to-end; errors are values

Data, evaluation, and agent layers return typed results/errors (not thrown exceptions across boundaries). A data failure, rate-limit, or agent failure each degrade to a partial result the UI can render. Rationale: the specs require honest partial states and readable errors.

D7 — Caching and rate limiting at the provider boundary

An in-memory (v1) cache keyed by ticker+dataset with short TTLs sits inside the provider layer, with retry/backoff for transient/rate-limit errors. Rationale: free tiers are tightly limited; caching one evaluation's repeated reads avoids burning quota. Persistence-backed cache is a later change.

D9 — Cost controls: capture real usage, price from a table, guard the budget

Every agent call's usage (input/output/cache tokens) and web-search count are captured from the API response — never estimated — and priced via a configurable table (per-model token prices + per-search price) so provider price changes are config, not code. Month-to-date spend is kept in lightweight local storage (a JSON file/localStorage-class store, not a database — consistent with the no-persistence scope) and checked before each evaluation against a configurable monthly budget: soft threshold warns, cap blocks. The pipeline evaluates the guard before dispatching the agent (and pre-empts a deep-dive whose projected cost exceeds the remaining budget), so a runaway loop can't blow the cap. Rationale: cost is the one resource a user can't see mid-run; real per-report numbers replace the design-time estimate and let the user tune the Opus/Fable mix. Alternative: rely on Anthropic-console billing after the fact — rejected because it's not in-app, not per-ticker, and offers no pre-emptive block.

D10 — Instrument-type-aware from the start; equity branch only in v1

The resolved profile and the Evaluation object carry an instrumentType discriminant (equity | etf). Instrument-agnostic sections (current standing, technicals, timing, entry/exit, stop-loss, macro) are shared; the fundamentals/valuation sections live under a per-type branch. v1 implements only the equity branch and returns an unsupported-type result for anything else. Rationale: an ETF is ~half the same (all technicals/timing) and ~half different (no EPS/earnings/segments; instead holdings, expense ratio, NAV premium/discount, weighted fundamentals). Baking the discriminant and branch seam in now is nearly free and avoids a refactor when the ETF branch is added as a fast-follow change. Alternative: equities-only with no seam — rejected as false economy given ETFs are an explicit near-term goal.

Roadmap (deferred, compose on this change)

  • ETF evaluation branch — fills the etf branch (holdings, expense ratio, NAV premium/discount, sector/geo weights, weighted fundamentals) and an ETF agent prompt variant; reuses everything instrument-agnostic here.
  • Screening / candidate-finder — a screening capability: criteria/thesis → grounded, ranked shortlist (fundamental screener filter + web verification) where each candidate links into this evaluator. Deliberately built after the evaluator exists, so every surfaced idea has a rigorous place to be checked rather than being trusted as an oracle.

Risks / Trade-offs

  • [Free-tier data is delayed/incomplete — segments, estimates, or history may be missing] → Normalized models mark fields unavailable; evaluation and UI render honestly; provider is swappable for a paid tier without consumer changes.
  • [Agent may hallucinate numbers] → Agent receives only the structured object, is instructed to cite nothing outside it, and prose is rendered alongside the computed tables so drift is visible; consider a post-check that every cited figure exists in the input.
  • [Rate limits during development/demo] → Caching + backoff; a fixture/mock provider for tests and offline work.
  • [Numeric correctness of valuation/technical math] → Pure modules with unit tests seeded from the DE example's known figures.
  • [Not financial advice / liability] → Mandatory disclaimer enforced in both the evaluation object and the agent narrative.

Migration Plan

Greenfield — no data migration. Deployment: run locally (localhost:3000) with .env.local holding the market-data and Anthropic keys. Rollout is incremental by capability (see tasks): scaffold → data layer + technicals → evaluation engine → agent → UI. Rollback is trivial (no persisted state). Keys are provided via env; absence degrades gracefully rather than failing the build.

Open Questions

  • Which free market-data provider is the default (coverage of segments + forward estimates varies materially)? Resolve early in tasks by spiking 2 providers on DE.
  • Source of options-implied expected move and forward consensus EPS on free tiers — may need a secondary source or graceful omission.
  • Preferred charting library (Recharts vs. lightweight-charts) for overlaying entry/exit/stop markers on price history.
  • Whether to add an automated "every cited figure exists in inputs" guard on agent output in v1 or defer to a later hardening change.