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>
This commit is contained in:
2026-08-21 17:03:04 -04:00
co-authored by Claude Opus 4.8
parent 73e93e7cb4
commit 902758ce67
44 changed files with 12425 additions and 91 deletions
@@ -55,14 +55,43 @@ stop levels, implied multiples at each level, and valuation reasoning are comput
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 default via `@anthropic-ai/sdk`
The agent takes the `Evaluation` object and returns narrative sections. Default
impl calls a current Claude model, instructed to use ONLY the supplied figures,
preserve any flagged caveats/discrepancies, and include the disclaimer. Structured
output (tool/JSON schema) is preferred so the UI can render narrative per section.
Alternative: free-text prompt with the raw data — rejected because grounding and
per-section rendering are weaker. When no key is set, the pipeline returns the
`Evaluation` with `narrative: unavailable`.
### 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 mode** — `claude -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
@@ -76,6 +105,44 @@ 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
@@ -24,8 +24,25 @@ stop-loss levels — with a data layer and agent interface designed to grow.
- Introduce a **pluggable analysis-agent layer** backed by **Claude via the
Anthropic SDK** (bring-your-own API key) that synthesizes the structured data
into a written investment thesis and the reasoning behind each recommendation.
- Scope v1 to **single-ticker deep evaluation**. Watchlist monitoring, alerts,
and multi-ticker dashboards are explicitly deferred to later changes.
The default model is **Claude Opus 4.8**, with a per-ticker "deep dive" option
that escalates to **Claude Fable 5** for the hardest analyses.
- Give the agent **live web grounding** (Anthropic's server-side web search and
web fetch) so macro/news facts the structured data layer lacks — tariff and rate
developments, analyst rating/target changes, management commentary, peer
read-throughs, dated catalysts — are sourced and attributed, matching the depth
of the reference evaluation.
- Introduce **cost controls**: capture per-report token/search usage, compute its
cost from a configurable price table, track month-to-date spend, and enforce a
configurable monthly budget (warn on a soft threshold, block on the cap) so the
agent — especially the Fable 5 deep dive — can't run up a surprise bill.
- Make the data models and evaluation engine **instrument-type-aware** (equity vs.
ETF) from the start. v1 builds the **equity** branch only; the ETF branch
(holdings, expense ratio, NAV premium/discount, weighted fundamentals) is a
fast-follow change that slots into the existing seams without a refactor.
- Scope v1 to **single-ticker deep evaluation** of individual equities. Deferred to
later changes: the ETF evaluation branch, a **screening / candidate-finder** mode
(criteria → grounded shortlist that feeds this evaluator), watchlist monitoring,
alerts, and multi-ticker dashboards.
## Capabilities
@@ -49,6 +66,10 @@ stop-loss levels — with a data layer and agent interface designed to grow.
request orchestration (data → evaluation → agent), and the report view that
presents fundamentals, valuation reasoning, macro factors, a price chart with
marked entry/exit/stop-loss levels, and the agent's written thesis.
- `cost-controls`: Per-report usage capture and cost computation from a
configurable price table, running spend aggregation, and a configurable monthly
budget guard (soft-threshold warning, hard-cap block) with the numbers surfaced
in the UI.
### Modified Capabilities
@@ -15,33 +15,74 @@ evaluation engine or UI.
- **WHEN** a different agent implementation is configured
- **THEN** the system uses it without changes to evaluation or UI code
### Requirement: Claude default implementation via Anthropic SDK
### Requirement: Default agent via Claude Code CLI; API as swappable alternate
The system SHALL ship a default `AnalysisAgent` backed by Claude through the
`@anthropic-ai/sdk`, targeting a current Claude model. The API key SHALL be
supplied by the user (bring-your-own-key) via configuration and never hard-coded
or committed.
The system SHALL ship a default `AnalysisAgent` that invokes the local **Claude
Code CLI** in headless mode (`claude -p --output-format json`), which runs on the
operator's existing Claude authentication (e.g. subscription) rather than a metered
API key. The system SHALL also ship an alternate implementation backed by the
Anthropic API (`@anthropic-ai/sdk`), selectable via configuration without changes
in consumers. Either way, the default model SHALL be **Claude Opus 4.8** (CLI alias
`opus`) and a per-request "deep dive" option SHALL escalate to **Claude Fable 5**
(CLI alias `fable`); the model SHALL be selectable per evaluation. Structured
narrative output SHALL be requested via the CLI `--json-schema` option (or the
equivalent structured-output mechanism on the API alternate). Any API key used by
the alternate SHALL be loaded from configuration and never hard-coded or committed.
#### Scenario: Claude produces the written thesis
- **WHEN** a valid Anthropic API key is configured and an evaluation is submitted
- **THEN** the default agent calls Claude and returns the written thesis
#### Scenario: Claude Code CLI produces the written thesis
- **WHEN** the CLI agent is configured and an evaluation is submitted
- **THEN** the system runs `claude -p` with model alias `opus` and returns the
written thesis parsed from the CLI JSON output
#### Scenario: Key sourced from configuration
- **WHEN** the app reads its configuration
#### Scenario: Deep-dive escalates to Fable 5
- **WHEN** an evaluation is requested with the deep-dive option enabled
- **THEN** the agent runs the CLI with model alias `fable` for that request
- **AND** requests without the option continue to use `opus`
#### Scenario: API alternate selected via configuration
- **WHEN** the agent transport is configured to the API alternate with a valid key
- **THEN** evaluations are served through `@anthropic-ai/sdk` instead of the CLI
- **AND** no changes to evaluation or UI code are required
#### Scenario: Secrets sourced from configuration
- **WHEN** the API alternate reads its configuration
- **THEN** the Anthropic API key is loaded from environment/config, not source code
### Requirement: Live web grounding
The agent SHALL be able to retrieve current information via its web search and
web fetch capability — the Claude Code CLI's `WebSearch`/`WebFetch` tools for the
default transport, or Anthropic's server-side web tools for the API alternate — to
source macro and news-driven facts the structured data layer does not provide:
tariff/rate developments, analyst rating and price-target changes, management
commentary, peer read-throughs, and dated catalysts. Facts drawn from the web SHALL
carry their source so the output can attribute them, and grounding SHALL be scoped
to the ticker under evaluation.
#### Scenario: Web-sourced facts are attributed
- **WHEN** the agent uses a web-sourced fact in the analysis (e.g. an analyst
target change or a catalyst date)
- **THEN** the output attributes that fact to its source
#### Scenario: Grounding degrades gracefully
- **WHEN** web search or fetch is unavailable or returns an error
- **THEN** the agent still produces the analysis from the structured evaluation
- **AND** notes that live macro/news grounding was unavailable
### Requirement: Grounded, structured written analysis
The agent SHALL base its narrative only on the supplied evaluation data and
SHALL NOT invent figures. It SHALL produce the narrative sections reflected in the
example evaluation: current standing, earnings recap and quality-of-earnings
caveats, valuation with explicit over/undervalued reasoning, macro factors, timing,
entry/exit/stop-loss rationale, bull-versus-bear, and an actionable plan. When a
data caveat or discrepancy is present in the input, the agent SHALL preserve it.
The agent SHALL base its narrative on the supplied evaluation data plus attributed
web-sourced facts, and SHALL NOT invent figures. It SHALL produce the narrative
sections reflected in the example evaluation: current standing, earnings recap and
quality-of-earnings caveats, valuation with explicit over/undervalued reasoning,
macro factors, timing, entry/exit/stop-loss rationale, bull-versus-bear, and an
actionable plan. When a data caveat or discrepancy is present in the input, the
agent SHALL preserve it.
#### Scenario: Narrative grounded in provided data
- **WHEN** the agent writes the analysis
- **THEN** every figure it cites is present in the supplied evaluation object
- **THEN** every figure it cites is either present in the supplied evaluation
object or attributed to a web source it retrieved
#### Scenario: Data caveats preserved
- **WHEN** the evaluation object flags a discrepancy or one-time item
@@ -51,16 +92,18 @@ data caveat or discrepancy is present in the input, the agent SHALL preserve it.
- **WHEN** the agent returns its analysis
- **THEN** the analysis-not-advice disclaimer is present
### Requirement: Graceful degradation without an API key
### Requirement: Graceful degradation without an available agent
When no analysis-agent key is configured, the system SHALL still return the full
structured evaluation and SHALL clearly indicate that the written narrative is
unavailable until a key is provided.
The system SHALL, when no analysis agent is available (the Claude Code CLI is not
installed or not authenticated, or the configured API alternate has no key), still
return the full structured evaluation and clearly indicate that the written
narrative is unavailable and how to enable it.
#### Scenario: No key configured
- **WHEN** an evaluation is requested and no agent API key is configured
#### Scenario: Agent unavailable
- **WHEN** an evaluation is requested and no agent transport is available
- **THEN** the structured evaluation is returned
- **AND** the response indicates the written analysis is unavailable pending a key
- **AND** the response indicates the written analysis is unavailable and how to
enable it (authenticate the CLI, or configure an API key)
### Requirement: Agent error handling
@@ -0,0 +1,93 @@
## ADDED Requirements
### Requirement: Per-report usage capture
The system SHALL capture the actual usage of every evaluation that calls the
analysis agent from the agent's own reported usage — the Claude Code CLI JSON
output (`total_cost_usd` plus token usage) for the default transport, or the API
response usage for the alternate — not from estimates. Captured fields SHALL
include, where the transport reports them, token counts, model used, and a reported
cost. When the transport runs on a subscription rather than metered billing, the
reported marginal cost MAY be zero or near-zero; the system SHALL record what the
transport reports.
#### Scenario: Usage recorded for a completed report
- **WHEN** an evaluation completes an agent call
- **THEN** the system records the report's token counts, web-search count, model,
ticker, and timestamp
#### Scenario: Usage captured on partial results
- **WHEN** an agent call fails after consuming tokens (e.g. a mid-stream error)
- **THEN** the usage consumed up to that point is still recorded
### Requirement: Cost computation from a configurable price table
The system SHALL compute each report's cost from captured usage and a configurable
price table (per-model input/output/cache token prices and per-search price).
Prices SHALL be editable via configuration so provider price changes require no
code changes.
#### Scenario: Report cost computed
- **WHEN** a report's usage is captured
- **THEN** the system computes its cost using the configured price table and stores
the cost alongside the usage
#### Scenario: Unknown model priced safely
- **WHEN** a report uses a model absent from the price table
- **THEN** the system flags the cost as unpriced rather than recording zero
### Requirement: Spend aggregation
The system SHALL maintain a running month-to-date total spend and SHALL expose
per-report cost history. The spend record MAY use lightweight local storage; a
full database is out of scope for this change.
#### Scenario: Month-to-date total available
- **WHEN** a new report's cost is recorded
- **THEN** the month-to-date total reflects the new report
- **AND** the per-report history includes the new entry
#### Scenario: Month boundary resets the running total
- **WHEN** the calendar month changes
- **THEN** the month-to-date total starts from zero for the new month
- **AND** prior months' totals remain available in history
### Requirement: Monthly budget guard
The system SHALL enforce a configurable monthly budget. When month-to-date spend
crosses a configurable soft threshold (default 80% of budget) the system SHALL warn
but still allow evaluations. When spend reaches the budget cap the system SHALL
block new evaluations and return a clear message rather than silently proceeding.
A disabled/zero budget SHALL mean no enforcement.
#### Scenario: Soft-threshold warning
- **WHEN** month-to-date spend crosses the soft threshold but is below the cap
- **THEN** the system allows the evaluation and surfaces a budget warning with the
current spend and remaining budget
#### Scenario: Hard-cap block
- **WHEN** month-to-date spend has reached the configured cap
- **THEN** the system blocks new evaluations and returns a message stating the cap
was reached and how to raise or disable it
#### Scenario: Deep-dive gated at the cap
- **WHEN** a deep-dive (Fable 5) evaluation is requested and the projected cost
would exceed the remaining budget
- **THEN** the system blocks it and explains the remaining budget
#### Scenario: Guard disabled
- **WHEN** the monthly budget is set to zero or disabled
- **THEN** evaluations proceed without budget enforcement
### Requirement: Cost visibility in the UI
The system SHALL display each report's cost in the report view and SHALL surface
month-to-date spend against the configured budget in the app.
#### Scenario: Report cost shown
- **WHEN** a report renders
- **THEN** its computed cost is shown in the report view
#### Scenario: Budget status shown
- **WHEN** the app is in use
- **THEN** month-to-date spend and remaining budget are visible
@@ -187,3 +187,24 @@ SHALL be represented explicitly rather than fabricated.
#### Scenario: Missing data is not fabricated
- **WHEN** a required input for a section is unavailable
- **THEN** that section marks the value unavailable rather than inventing one
### Requirement: Instrument-type-aware evaluation
The evaluation SHALL carry the resolved instrument type (`equity` or `etf`) as a
discriminant, and the fundamentals-driven sections (earnings recap,
quality-of-earnings, financial/segment breakdown, valuation, over/undervalued
reasoning) SHALL be modeled as the **equity** branch so an ETF branch (holdings,
expense ratio, NAV premium/discount, weighted fundamentals) can be added later
without changing the shared, instrument-agnostic sections (current standing,
technicals, timing, entry/exit, stop-loss, macro). v1 SHALL implement the equity
branch; requesting an evaluation for a non-equity type SHALL return an
unsupported-type result rather than running the equity branch.
#### Scenario: Equity evaluated on the equity branch
- **WHEN** an evaluation runs for an `equity` instrument
- **THEN** the equity fundamentals/valuation sections are produced
#### Scenario: Non-equity type not forced through the equity branch
- **WHEN** an evaluation is requested for an `etf` (or other non-equity) instrument
- **THEN** the system returns an unsupported-type result
- **AND** does not populate equity-only sections with fabricated values
@@ -21,15 +21,25 @@ consumers.
### Requirement: Ticker resolution for NYSE and Nasdaq
The system SHALL accept a ticker symbol, validate that it resolves to an
NYSE- or Nasdaq-listed equity, and return a normalized company profile
(name, exchange, sector/industry, currency, shares outstanding).
NYSE- or Nasdaq-listed security, and return a normalized profile that includes an
**instrument type** discriminant (`equity` or `etf`) plus name, exchange, currency,
and — for equities — sector/industry and shares outstanding. v1 fully supports the
`equity` type; ETF-specific data is a later change, but resolution SHALL still
classify ETFs so an unsupported-type path is explicit rather than silently wrong.
#### Scenario: Valid listed ticker resolves
- **WHEN** a user submits a ticker listed on NYSE or Nasdaq (e.g. `DE`)
- **THEN** the system returns a company profile with name, exchange, and sector
#### Scenario: Valid listed equity resolves
- **WHEN** a user submits an equity ticker listed on NYSE or Nasdaq (e.g. `DE`)
- **THEN** the system returns a profile with instrument type `equity`, name,
exchange, and sector
#### Scenario: ETF is classified, not misread as an equity
- **WHEN** a user submits an ETF ticker (e.g. `SPY`)
- **THEN** the system returns a profile with instrument type `etf`
- **AND** the app reports ETF evaluation as not yet supported rather than running
the equity branch against it
#### Scenario: Unknown or unlisted ticker is rejected
- **WHEN** a user submits a symbol that does not resolve to an NYSE/Nasdaq equity
- **WHEN** a user submits a symbol that does not resolve to an NYSE/Nasdaq security
- **THEN** the system returns a not-found result with a clear message
- **AND** no evaluation is attempted
+65 -51
View File
@@ -1,73 +1,87 @@
## 1. Project scaffold
- [ ] 1.1 Initialize Next.js (App Router) + TypeScript project at the repo root
- [ ] 1.2 Add Tailwind CSS and a base layout/theme (light + dark)
- [ ] 1.3 Add a charting library (Recharts or lightweight-charts) and confirm it renders
- [ ] 1.4 Set up `.env.local` handling and `.env.example` for `MARKET_DATA_API_KEY` and `ANTHROPIC_API_KEY`; ensure secrets never reach the client bundle
- [ ] 1.5 Configure lint/format/test tooling (ESLint, Prettier, Vitest/Jest) and a passing sample test
- [ ] 1.6 Define shared TypeScript types module (normalized data models + `Evaluation` object skeleton with `unavailable` markers)
- [x] 1.1 Initialize Next.js (App Router) + TypeScript project at the repo root
- [x] 1.2 Add Tailwind CSS and a base layout/theme (light + dark)
- [x] 1.3 Add a charting library (Recharts or lightweight-charts) and confirm it renders
- [x] 1.4 Set up `.env.local` handling and `.env.example` for `MARKET_DATA_API_KEY` and `ANTHROPIC_API_KEY`; ensure secrets never reach the client bundle
- [x] 1.5 Configure lint/format/test tooling (ESLint, Prettier, Vitest/Jest) and a passing sample test
- [x] 1.6 Define shared TypeScript types module (normalized data models + `Evaluation` object skeleton with `unavailable` markers) with an `instrumentType` discriminant (`equity` | `etf`); fundamentals/valuation sections under a per-type branch, instrument-agnostic sections shared
## 2. Market-data layer (`market-data` spec)
- [ ] 2.1 Define the `DataProvider` interface and normalized models (`CompanyProfile`, `Fundamentals` incl. optional segments, `PriceHistory`, `AnalystCoverage`, `Estimates`), each with `asOf` + `delayed|realtime`
- [ ] 2.2 Spike two free providers (e.g. FMP, Finnhub) against `DE`; pick the default based on segment + forward-estimate coverage; record the choice in design Open Questions
- [ ] 2.3 Implement the default free-tier provider behind the interface
- [ ] 2.4 Implement ticker resolution with NYSE/Nasdaq validation and typed not-found result
- [ ] 2.5 Implement a fixture/mock provider seeded from the DE example for tests and offline dev
- [ ] 2.6 Add provider-boundary caching (in-memory, TTL, keyed by ticker+dataset) and retry/backoff for rate-limit/transient errors returning typed errors
- [ ] 2.7 Unit-test normalization, unavailable-field handling, and error/rate-limit paths
- [x] 2.1 Define the `DataProvider` interface and normalized models (`CompanyProfile`, `Fundamentals` incl. optional segments, `PriceHistory`, `AnalystCoverage`, `Estimates`), each with `asOf` + `delayed|realtime`
- [x] 2.2 Spike two free providers (e.g. FMP, Finnhub) against `DE`; pick the default based on segment + forward-estimate coverage; record the choice in design Open Questions
- [x] 2.3 Implement the default free-tier provider behind the interface
- [x] 2.4 Implement ticker resolution with NYSE/Nasdaq validation, an `instrumentType` (`equity`/`etf`) classification, and a typed not-found result
- [x] 2.5 Implement a fixture/mock provider seeded from the DE example for tests and offline dev
- [x] 2.6 Add provider-boundary caching (in-memory, TTL, keyed by ticker+dataset) and retry/backoff for rate-limit/transient errors returning typed errors
- [x] 2.7 Unit-test normalization, unavailable-field handling, and error/rate-limit paths
## 3. Technicals module (`market-data` spec)
- [ ] 3.1 Implement pure functions: 20/50/200-day moving averages, 52-week high/low distance, average volume + today's volume multiple
- [ ] 3.2 Implement swing high/low detection and realized (e.g. 60-day annualized) volatility
- [ ] 3.3 Handle insufficient-history cases (mark long-window indicators unavailable)
- [ ] 3.4 Unit-test all indicators against known values from the DE example
- [x] 3.1 Implement pure functions: 20/50/200-day moving averages, 52-week high/low distance, average volume + today's volume multiple
- [x] 3.2 Implement swing high/low detection and realized (e.g. 60-day annualized) volatility
- [x] 3.3 Handle insufficient-history cases (mark long-window indicators unavailable)
- [x] 3.4 Unit-test all indicators against known values from the DE example
## 4. Evaluation engine (`equity-evaluation` spec)
- [ ] 4.1 Implement current-standing summary (price, day change abs/%, volume multiple, 52-wk distance, market cap, trailing P/E)
- [ ] 4.2 Implement earnings recap (EPS vs consensus, beat/miss, net income YoY, guidance changes) gated by a configurable recency window
- [ ] 4.3 Implement quality-of-earnings caveats (one-time items + EPS impact, price-vs-volume, wrong-baseline headline metrics)
- [ ] 4.4 Implement financial summary (current vs prior-year) and per-segment breakdown when available
- [ ] 4.5 Implement valuation view (TTM EPS, trailing/forward P/E, growth, FCF & dividend yield, vs own history) with conflicting-estimate reconciliation
- [ ] 4.6 Implement explicit over/undervalued reasoning (multiple-expansion vs earnings-growth, trough/peak earnings)
- [ ] 4.7 Implement macro/sector factor analysis (tariffs trajectory, rates, input-cost pressure, peer read-throughs)
- [ ] 4.8 Implement timing/volatility context (post-earnings behavior, implied vs actual move, monthly ranges, gap-fill status)
- [ ] 4.9 Implement entry levels (ranked levels with meaning, % below price, implied multiple at each) grouped into bands with starter/high-conviction tranches
- [ ] 4.10 Implement exit/targets (analyst avg/median/range + technical resistance) and stop-loss levels (technical/volatility-based, concrete prices + rationale)
- [ ] 4.11 Implement bull-vs-bear synthesis and actionable plan (tranche sizing, next dated catalyst, conditional rules, one metric to watch)
- [ ] 4.12 Emit the typed `Evaluation` object with explicit unavailable markers (no fabrication) + mandatory disclaimer
- [ ] 4.13 Unit-test each section, including missing-input and unavailable-value paths, seeded from the DE example
- [x] 4.1 Implement current-standing summary (price, day change abs/%, volume multiple, 52-wk distance, market cap, trailing P/E)
- [x] 4.2 Implement earnings recap (EPS vs consensus, beat/miss, net income YoY, guidance changes) gated by a configurable recency window
- [x] 4.3 Implement quality-of-earnings caveats (one-time items + EPS impact, price-vs-volume, wrong-baseline headline metrics)
- [x] 4.4 Implement financial summary (current vs prior-year) and per-segment breakdown when available
- [x] 4.5 Implement valuation view (TTM EPS, trailing/forward P/E, growth, FCF & dividend yield, vs own history) with conflicting-estimate reconciliation
- [x] 4.6 Implement explicit over/undervalued reasoning (multiple-expansion vs earnings-growth, trough/peak earnings)
- [x] 4.7 Implement macro/sector factor analysis (tariffs trajectory, rates, input-cost pressure, peer read-throughs)
- [x] 4.8 Implement timing/volatility context (post-earnings behavior, implied vs actual move, monthly ranges, gap-fill status)
- [x] 4.9 Implement entry levels (ranked levels with meaning, % below price, implied multiple at each) grouped into bands with starter/high-conviction tranches
- [x] 4.10 Implement exit/targets (analyst avg/median/range + technical resistance) and stop-loss levels (technical/volatility-based, concrete prices + rationale)
- [x] 4.11 Implement bull-vs-bear synthesis and actionable plan (tranche sizing, next dated catalyst, conditional rules, one metric to watch)
- [x] 4.12 Emit the typed `Evaluation` object with explicit unavailable markers (no fabrication) + mandatory disclaimer; carry `instrumentType`, populate the equity branch, and return an unsupported-type result for non-equity instruments (no equity-branch fabrication)
- [x] 4.13 Unit-test each section, including missing-input and unavailable-value paths, seeded from the DE example
## 5. Analysis agent (`analysis-agent` spec)
- [ ] 5.1 Define the `AnalysisAgent` interface (input: `Evaluation`; output: narrative sections)
- [ ] 5.2 Implement the Claude default via `@anthropic-ai/sdk` (current model), key loaded server-side from config
- [ ] 5.3 Design the prompt + structured/JSON output so narrative maps to spec sections and uses ONLY supplied figures
- [ ] 5.4 Preserve input caveats/discrepancies and enforce the disclaimer in the narrative
- [ ] 5.5 Implement graceful degradation when no key is set (return structured evaluation, mark narrative unavailable)
- [ ] 5.6 Implement typed agent error handling (auth/rate-limit/timeout) that retains the computed evaluation
- [ ] 5.7 Test grounding (every cited figure exists in input), no-key, and error paths with a mock agent
- [x] 5.1 Define the `AnalysisAgent` interface (input: `Evaluation` + options incl. deep-dive flag; output: narrative sections + sources + reported usage/cost) with a config-selected transport
- [x] 5.2 Implement the default Claude Code CLI transport: spawn `claude -p --output-format json --model <opus|fable> --json-schema <narrative schema> --append-system-prompt <instructions> --allowedTools "WebSearch WebFetch" --permission-mode dontAsk`; parse the JSON result (structured output + `total_cost_usd` + usage)
- [x] 5.3 Implement the alternate `@anthropic-ai/sdk` transport (config-selected): Opus 4.8 / Fable 5 with `thinking: {type: "adaptive"}` (no `budget_tokens`/sampling), streaming, Fable 5 refusal fallback to Opus 4.8, server-side web tools
- [x] 5.4 Per-request model selection (`opus` default, `fable` for deep-dive) wired through both transports; ground with web search/fetch scoped to the ticker and capture source attributions
- [x] 5.5 Design the prompt + structured schema so narrative maps to spec sections and cites only supplied figures or attributed web sources
- [x] 5.6 Preserve input caveats/discrepancies and enforce the disclaimer in the narrative
- [x] 5.7 Implement graceful degradation: CLI missing/unauthenticated and no API key → structured evaluation with narrative unavailable + how-to-enable; web grounding error → analysis from structured data, note grounding unavailable
- [x] 5.8 Implement typed agent error handling (spawn failure/non-zero exit/timeout for CLI; auth/rate-limit/timeout for API) that retains the computed evaluation
- [x] 5.9 Test model selection, transport selection (CLI vs API), grounding attribution, agent-unavailable, grounding-error, and refusal-fallback paths with a mock/stubbed transport
## 6. Pipeline + API (`evaluation-app` spec)
- [ ] 6.1 Implement server-side route/action orchestrating resolve → data → evaluate → agent, returning `{ evaluation, narrative | error }`
- [ ] 6.2 Handle partial results (data ok / agent unavailable) and typed errors end-to-end
- [ ] 6.3 Ensure secret keys are used only server-side and surface missing-key state per capability
- [x] 6.1 Implement server-side route/action orchestrating resolve → data → evaluate → agent, returning `{ evaluation, narrative | error }`
- [x] 6.2 Handle partial results (data ok / agent unavailable) and typed errors end-to-end
- [x] 6.3 Ensure secret keys are used only server-side and surface missing-key state per capability
## 7. Web UI (`evaluation-app` spec)
- [ ] 7.1 Build the ticker search entry point with inline invalid/not-found handling
- [ ] 7.2 Build the report view rendering every section in order, tables as tables, unavailable values shown honestly
- [ ] 7.3 Build the price chart with moving averages and overlay markers for entry bands, exit/targets, and stop-loss levels
- [ ] 7.4 Implement loading state and readable error states (no blank screens)
- [ ] 7.5 Surface missing-key indicators and a settings/config affordance for keys
- [x] 7.1 Build the ticker search entry point with inline invalid/not-found handling and a clear "ETF evaluation not yet supported" message when an `etf` is resolved
- [x] 7.2 Build the report view rendering every section in order, tables as tables, unavailable values shown honestly
- [x] 7.3 Build the price chart with moving averages and overlay markers for entry bands, exit/targets, and stop-loss levels
- [x] 7.4 Implement loading state and readable error states (no blank screens)
- [x] 7.5 Surface missing-key indicators and a settings/config affordance for keys
- [x] 7.6 Add a "deep dive" toggle on the search/report view that requests the Fable 5 model, and render web-source attributions in the report
## 8. Verification
## 8. Cost controls (`cost-controls` spec)
- [ ] 8.1 End-to-end run on `DE` (with keys) producing a report structurally matching the example; note any data gaps from the free tier
- [ ] 8.2 End-to-end run with no agent key confirming structured evaluation still renders
- [ ] 8.3 Run the full test suite and lint; confirm green
- [ ] 8.4 Update README with setup, env vars, provider choice, and run instructions
- [ ] 8.5 Run `openspec validate stock-deep-evaluation` and archive the change when complete
- [x] 8.1 Define a configurable price table (per-model input/output/cache token prices + per-search price) seeded with current Opus 4.8 and Fable 5 rates
- [x] 8.2 Capture per-report usage from the agent API response (tokens, cache tokens, web-search count, model) including on partial/error results
- [x] 8.3 Compute per-report cost from usage + price table; flag unpriced models rather than recording zero
- [x] 8.4 Implement lightweight spend storage: per-report history + month-to-date total with month-boundary reset (no database)
- [x] 8.5 Implement the monthly budget guard: soft-threshold warning, hard-cap block, deep-dive pre-emption, and disabled-when-zero; check before dispatching the agent
- [x] 8.6 Surface per-report cost in the report view and month-to-date spend vs budget in the app
- [x] 8.7 Unit-test cost computation, month reset, soft/hard thresholds, deep-dive pre-emption, and the disabled-guard path
## 9. Verification
- [x] 9.1 End-to-end run on `DE` (with keys) producing a report structurally matching the example; note any data gaps from the free tier
- [x] 9.2 End-to-end run with no agent key confirming structured evaluation still renders
- [x] 9.3 Verify cost is captured and displayed, and that the budget guard warns and blocks at its thresholds
- [x] 9.4 Run the full test suite and lint; confirm green
- [x] 9.5 Update README with setup, env vars, provider choice, and run instructions
- [x] 9.6 Run `openspec validate stock-deep-evaluation` and archive the change when complete