Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ help-full:
@echo "Preview-first fundamentals and universe imports:"
@echo " export SEC_USER_AGENT='Name email@example.com'"
@echo " make sec-stage TICKERS=NVDA,MSFT"
@echo " make sec-fundamentals-preview TICKERS=AAPL,NVDA,AMD Official SEC annual comparison; max five explicit tickers; no cache, staging, or apply writes"
@echo " make yfinance-stage TICKERS=NVDA"
@echo " make fundamentals-source-ladder TICKERS=NVDA"
@echo " Try SEC, yfinance, FMP, Alpha Vantage, then Finnhub before stopping at reviewed blocker evidence"
Expand Down Expand Up @@ -1271,6 +1272,13 @@ endif
--primary-document "$(or $(PRIMARY_DOCUMENT),nvda-20260426.htm)" \
--as-of "$(AS_OF)"

.PHONY: sec-fundamentals-preview
sec-fundamentals-preview:
ifndef TICKERS
$(error TICKERS is required, for example: make sec-fundamentals-preview TICKERS=AAPL,NVDA,AMD)
endif
@PYTHONDONTWRITEBYTECODE=1 python3 -m src.sec_fundamentals_preview --tickers "$(TICKERS)"

demo-dashboard-render-smoke:
@STOCK_RESEARCH_DATA_PROFILE=demo python3 -m src.dashboard_render_smoke

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The report is not a black box: local data rows provide inputs, and project rules
## Current Snapshot
The local sample tracks a broad stock universe, with a smaller subset ready for each analysis feature. Exact universe and ready counts can change after local refresh/import work, so use `make readiness-ops-center` for current lane truth. Treat `make status-check TOP_N=5` and dashboard counts as saved generated-snapshot context, not current-market freshness proof.
Read the counts in three layers: master universe for broad coverage planning, active universe for the demo/research workflow, and analysis-ready subsets for DCF, peer context, or candidate review. A tracked ticker is not automatically ready for every analysis family; blocked rows stay visibly locked.
Visitor status: the product workflow, dashboard, single-stock reports, readiness gates, visitor path, and public checks are working. Broad fundamentals, DCF, peers, earnings, and analyst estimates remain visibly blocked by missing trusted data until trusted rows exist, so those gaps should be read as source-proof work rather than broken analysis.
Visitor status: the product workflow, dashboard, single-stock reports, readiness gates, visitor path, and public checks are working. Broad fundamentals, DCF, peers, earnings, and analyst estimates remain visibly blocked by missing trusted data until trusted rows exist, so those gaps should be read as source-proof work rather than broken analysis. **No-key SEC actuals inspection:** run `make sec-fundamentals-preview TICKERS=AAPL,NVDA,AMD` to compare annual facts from official SEC endpoints for at most five explicit tickers. The command is inspection-only: it writes no cache, import, canonical, readiness, or output files and does not authorize a data apply. The packet records field-level period, unit, retrieval, source-rights, schema, delta, and owner-action evidence. Only direct fields already allowed by the registered source scope can become future owner-review candidates; derived, mixed-unit, out-of-scope, and incoherent-period fields remain blocked, and the result does not activate product readiness.
## External Reviewer Handoff
Use this as the short GitHub/LinkedIn review path before reading operator detail:
| Question | Short answer |
Expand Down
66 changes: 66 additions & 0 deletions docs/superpowers/plans/2026-08-15-sec-fundamentals-preview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SEC Fundamentals No-Write Preview Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:test-driven-development for each behavior change. Track steps with checkbox (`- [ ]`) syntax.

**Goal:** Build a capped official-SEC comparison that exposes coherent annual actual candidates and their blockers without modifying canonical, staged, cached, readiness, or generated data.

**Architecture:** Extend the existing SEC adapter with true no-cache reads and private per-field provenance. Add a pure comparison module that reads canonical/staged headers, fetches only official SEC JSON in memory, classifies field coherence and rights, and prints deterministic JSON. Expose it through one Make target.

**Tech Stack:** Python 3.12, urllib, pandas, YAML-backed source-rights registry, argparse, JSON, pytest, Make.

## Global Constraints

- Maximum five explicit tickers; first audited cohort AAPL, AMZN, GOOG.
- Official SEC ticker-map and Companyfacts endpoints only.
- No provider fallback, canonical apply, import staging, cache write, readiness mutation, or rights change.
- Missing facts remain unavailable; derived values are labelled derived and blocked pending exact field-scope review.
- Stage only explicit source/docs/test/Make paths. Never stage `data/`, `outputs/`, caches, or imports.

### Task 1: True No-Cache SEC Adapter

**Files:**
- Modify: `src/providers/sec_companyfacts.py`
- Modify: `tests/test_sec_companyfacts.py`

- [ ] Write RED tests proving ticker-map and Companyfacts no-cache calls make the expected official request but create no cache directory or file.
- [ ] Add a `cache=False` ticker-map path and avoid resolving a Companyfacts cache path when cache is disabled.
- [ ] Preserve existing cached staging behavior and rerun the full SEC provider test file.

### Task 2: Provenance-Aware Candidate Extraction

**Files:**
- Modify: `src/providers/sec_companyfacts.py`
- Modify: `tests/test_sec_companyfacts.py`

- [ ] Write RED tests for direct record metadata and derived component provenance.
- [ ] Add private `_field_provenance` output to the extractor; confirm staging rows still omit private keys.
- [ ] Prove missing facts stay `None` and no derived field is described as directly reported.

### Task 3: Pure Preview Comparison

**Files:**
- Create: `src/sec_fundamentals_preview.py`
- Create: `tests/test_sec_fundamentals_preview.py`

- [ ] Write RED tests for explicit input, five-ticker cap, deterministic field deltas, classification precedence, period/accession coherence, malformed payloads, staged schema deltas, AAPL mixed-period visibility, and GOOG missing shares.
- [ ] Implement input parsing, official fetch orchestration, canonical/staged read-only projection, field comparison, coherence checks, rights review, and deterministic JSON rendering.
- [ ] Keep one ticker's failure isolated and report it without fabricated values.

### Task 4: Command Surface

**Files:**
- Modify: `Makefile`
- Modify: `tests/test_launchers.py`

- [ ] Write RED tests for help text and exact command wiring.
- [ ] Add `make sec-fundamentals-preview TICKERS=AAPL,AMZN,GOOG`; require `TICKERS` and pass no output/cache/apply argument.
- [ ] Prove the target cannot invoke stage, apply, readiness, or fallback-provider commands.

### Task 5: Cohort Audit and Verification

- [ ] Run focused provider, preview, launcher, stock-report, and data-quality tests.
- [ ] Run targeted Ruff/compile checks and `git diff --check`.
- [ ] Compare all tracked `data/`/`outputs/` hashes and both ignored SEC-state hashes to their Stage 0 manifests.
- [ ] Run the live AAPL/AMZN/GOOG preview once with the configured SEC user agent and save any durable evidence only under a fresh `/tmp` directory.
- [ ] Self-review the complete diff for Critical/Important issues and resolve them before local commits.
- [ ] Commit only named source/docs/tests/Make paths. Do not push.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# SEC Fundamentals No-Write Preview Design

**Status:** Approved for bounded local implementation. Canonical apply, source-rights expansion, release recording, and remote synchronization are not authorized.

## Purpose

Provide a deterministic, inspection-only comparison between the current canonical fundamentals row and a fresh official SEC Companyfacts candidate for at most five explicitly named tickers. The first cohort is AAPL, AMZN, and GOOG.

The preview exists to expose stale, mixed-period, missing, unsupported, and derived evidence. It does not promote readiness or decide that a candidate may be published.

## Request and No-Write Boundary

- Accept only explicit tickers and reject an empty list or more than five unique tickers.
- Fetch only the SEC ticker map and SEC Companyfacts HTTPS endpoints with an identifying `SEC_USER_AGENT`.
- Do not call any fallback provider, scraper, search result, or paid API.
- Add a true no-cache adapter path: when cache is disabled, neither ticker-map nor Companyfacts path resolution may create directories or files.
- Read canonical `data/fundamentals.csv` and, when present, the ignored staged fundamentals header only for comparison. Never normalize, rewrite, delete, or apply either file.
- Print deterministic JSON to stdout. Tests may use an isolated `/tmp` fixture, but production code has no output-file option.

## Candidate and Provenance Model

The existing SEC extractor remains the source of candidate calculations. It will also expose private field-provenance metadata that the staging writer omits. Each field reports:

- canonical and SEC candidate values;
- `changed`, `unchanged`, or `missing` value status;
- fiscal period start/end when applicable;
- filing date, accession, form, taxonomy, concept, unit, and exact SEC Companyfacts URL;
- `direct` or `derived` value kind;
- one fail-closed classification and its publishability blocker.

The preview covers the SEC-backed canonical fields currently produced by the extractor: revenue, revenue growth, EPS, free cash flow, FCF margin, profit margin, operating margin, EBITDA, cash, debt, and shares outstanding. Source components such as operating income, cash from operations, and capital expenditures remain provenance inputs, not silently added canonical columns.

## Coherence Rules

Revenue's latest annual record defines the candidate fiscal-period anchor. Annual flow facts must use that period. Instant facts must be tied to the same filing accession or exact period end. Derived fields inherit every component's context. Revenue growth may intentionally use the anchored annual period and its immediately prior annual period.

When a component cannot be tied to that context, the observed value may be shown for diagnosis but is classified `period_conflict` or `source_context_ambiguous` and is blocked from publication. Missing facts remain unavailable; they never become zero.

## Source-Rights Classification

Classifications are resolved in this order:

1. `missing` when no candidate fact exists.
2. `period_conflict` when a period-specific fact conflicts with the annual anchor.
3. `source_context_ambiguous` when filing/accession context cannot be tied to the anchor.
4. `derived_scope_review_required` for every calculated field, even when its source components are registered.
5. `approved_direct` only when the exact direct field is listed for `sec_companyfacts` in `config/source_rights.yml`.
6. `unsupported` for a present direct field outside the registered field scope.

No preview result changes the source-rights registry. Filing metadata is evidence context, not permission to publish an otherwise unsupported field.

## Schema Risk

The result reports:

- candidate provenance components that are not canonical columns;
- canonical columns not produced by this SEC candidate;
- columns found in the ignored staged fundamentals file but absent from canonical fundamentals;
- columns that a naïve full-row rewrite would drop or add.

The preview never adds `currency` or any other column and never rewrites the full dataset for one ticker.

## Failure States

- Missing `SEC_USER_AGENT`, invalid ticker input, a non-SEC request, malformed payload, unresolved CIK, or missing canonical row is reported explicitly and fails closed.
- One ticker's failure does not fabricate values for that ticker or change another ticker's result.
- Analyst estimates, targets, ratings, recommendations, prices, peer decisions, and quarterly cash-flow derivations are outside this command.

## Verification

Tests must prove the ticker cap, official-endpoint restriction, true no-cache behavior, deterministic deltas, direct-versus-derived classification, missing and malformed fail-closed behavior, mixed-period blocking, staged schema reporting, AAPL mixed-period visibility, and GOOG explicit-share unavailability. Before and after hashes must prove that tracked `data/` and `outputs/` plus the existing ignored cache/staging files remain byte-identical.
132 changes: 109 additions & 23 deletions src/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -11487,24 +11487,51 @@ def _atr_or_volatility_source_label(source: object) -> str:
return "Volatility source unavailable"


def _screener_context_value(values: Mapping[str, object], field: str) -> object:
"""Read serialized screener fields without assuming one key casing."""

if field in values:
return values.get(field)
normalized = field.casefold()
return next(
(value for key, value in values.items() if str(key).casefold() == normalized),
None,
)


def stock_report_technical_context_cards(report_payload: dict[str, object]) -> list[dict[str, object]]:
screener_context = report_payload.get("screener_context", {}) or {}
momentum = screener_context.get("momentum_leaders", {}) or {}
watchlist = screener_context.get("final_watchlist", {}) or {}
setup_status = format_missing(momentum.get("SetupStatus") or watchlist.get("SetupStatus"), "Not available")
final_state = format_missing(watchlist.get("FinalState"), "Not available")
rs_percentile = momentum.get("RSPercentile")
relative_spy = momentum.get("RelativeReturnVsSPY")
relative_qqq = momentum.get("RelativeReturnVsQQQ")
volume_ratio = momentum.get("VolumeRatio")
setup_status = format_missing(
_screener_context_value(momentum, "SetupStatus")
or _screener_context_value(watchlist, "SetupStatus"),
"Not available",
)
final_state = format_missing(
_screener_context_value(watchlist, "FinalState"),
"Not available",
)
rs_percentile = _screener_context_value(momentum, "RSPercentile")
relative_spy = _screener_context_value(momentum, "RelativeReturnVsSPY")
relative_qqq = _screener_context_value(momentum, "RelativeReturnVsQQQ")
volume_ratio = _screener_context_value(momentum, "VolumeRatio")
volume_ratio_display = report_display_value(volume_ratio, "number")
volume_title = f"Volume {volume_ratio_display}x" if volume_ratio_display != "Not available" else "Volume ratio not available"
volatility_proxy = momentum.get("ATRorVolatilityPct")
volatility_source = _atr_or_volatility_source_label(momentum.get("ATRorVolatilitySource"))
volatility_proxy = _screener_context_value(momentum, "ATRorVolatilityPct")
volatility_source = _atr_or_volatility_source_label(
_screener_context_value(momentum, "ATRorVolatilitySource")
)
ma_stack = [
_technical_distance_label(momentum.get("DistanceFrom10EMA"), "10 EMA"),
_technical_distance_label(momentum.get("DistanceFrom21EMA"), "21 EMA"),
_technical_distance_label(momentum.get("DistanceFrom50SMA"), "50 SMA"),
_technical_distance_label(
_screener_context_value(momentum, "DistanceFrom10EMA"), "10 EMA"
),
_technical_distance_label(
_screener_context_value(momentum, "DistanceFrom21EMA"), "21 EMA"
),
_technical_distance_label(
_screener_context_value(momentum, "DistanceFrom50SMA"), "50 SMA"
),
]
return [
{
Expand Down Expand Up @@ -11681,18 +11708,77 @@ def stock_report_technical_context_frame(report_payload: dict[str, object]) -> p
momentum = screener_context.get("momentum_leaders", {}) or {}
watchlist = screener_context.get("final_watchlist", {}) or {}
rows = [
{"Metric": "Setup Status", "Value": format_missing(momentum.get("SetupStatus") or watchlist.get("SetupStatus"))},
{"Metric": "Final State", "Value": format_missing(watchlist.get("FinalState"))},
{"Metric": "RS Percentile", "Value": report_display_value(momentum.get("RSPercentile"), "number")},
{"Metric": "Relative Return vs SPY", "Value": report_display_value(momentum.get("RelativeReturnVsSPY"), "percent")},
{"Metric": "Relative Return vs QQQ", "Value": report_display_value(momentum.get("RelativeReturnVsQQQ"), "percent")},
{"Metric": "10 EMA Distance", "Value": report_display_value(momentum.get("DistanceFrom10EMA"), "percent")},
{"Metric": "21 EMA Distance", "Value": report_display_value(momentum.get("DistanceFrom21EMA"), "percent")},
{"Metric": "50 SMA Distance", "Value": report_display_value(momentum.get("DistanceFrom50SMA"), "percent")},
{"Metric": "Average Volume 20D", "Value": report_display_value(momentum.get("AvgVolume20D"), "integer")},
{"Metric": "Volume Ratio", "Value": report_display_value(momentum.get("VolumeRatio"), "number")},
{"Metric": "ATR / Volatility Proxy", "Value": report_display_value(momentum.get("ATRorVolatilityPct"), "percent")},
{"Metric": "Volatility Source", "Value": _atr_or_volatility_source_label(momentum.get("ATRorVolatilitySource"))},
{
"Metric": "Setup Status",
"Value": format_missing(
_screener_context_value(momentum, "SetupStatus")
or _screener_context_value(watchlist, "SetupStatus")
),
},
{
"Metric": "Final State",
"Value": format_missing(_screener_context_value(watchlist, "FinalState")),
},
{
"Metric": "RS Percentile",
"Value": report_display_value(
_screener_context_value(momentum, "RSPercentile"), "number"
),
},
{
"Metric": "Relative Return vs SPY",
"Value": report_display_value(
_screener_context_value(momentum, "RelativeReturnVsSPY"), "percent"
),
},
{
"Metric": "Relative Return vs QQQ",
"Value": report_display_value(
_screener_context_value(momentum, "RelativeReturnVsQQQ"), "percent"
),
},
{
"Metric": "10 EMA Distance",
"Value": report_display_value(
_screener_context_value(momentum, "DistanceFrom10EMA"), "percent"
),
},
{
"Metric": "21 EMA Distance",
"Value": report_display_value(
_screener_context_value(momentum, "DistanceFrom21EMA"), "percent"
),
},
{
"Metric": "50 SMA Distance",
"Value": report_display_value(
_screener_context_value(momentum, "DistanceFrom50SMA"), "percent"
),
},
{
"Metric": "Average Volume 20D",
"Value": report_display_value(
_screener_context_value(momentum, "AvgVolume20D"), "integer"
),
},
{
"Metric": "Volume Ratio",
"Value": report_display_value(
_screener_context_value(momentum, "VolumeRatio"), "number"
),
},
{
"Metric": "ATR / Volatility Proxy",
"Value": report_display_value(
_screener_context_value(momentum, "ATRorVolatilityPct"), "percent"
),
},
{
"Metric": "Volatility Source",
"Value": _atr_or_volatility_source_label(
_screener_context_value(momentum, "ATRorVolatilitySource")
),
},
]
return pd.DataFrame(rows)

Expand Down
Loading