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
@@ -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