Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
5b25fbc
refactor(data-pipeline): B1 provider seam 抽取,yfinance 收口到 providers/
HRLoveFun Sep 10, 2026
8a53f31
refactor(data-pipeline): B2 canonical store 表名落地(raw_bars/clean_bars/…
HRLoveFun Sep 10, 2026
206f03f
fix(migrate): dry-run 零副作用 + 补测试
HRLoveFun Sep 10, 2026
a1104aa
refactor(data-pipeline): B3 包重排为六阶段(providers/store/ingest/transform/…
HRLoveFun Sep 10, 2026
94447c0
refactor(core): B4 关闭 L1 core-purity,取数下沉到 services
HRLoveFun Sep 10, 2026
5c4e0d8
feat(readiness): B5 提交即规划 + 预取(ADR 0012),§8 Q1 定为 manifest
HRLoveFun Sep 10, 2026
a1ca7fd
feat(ui): B6 ticker-only 常驻参数栏 + Portfolio 页签(§8 Q3 定为独立面板)
HRLoveFun Sep 10, 2026
c298731
feat(params): B7 后端半批——/render 改为按模块 query args 取参(进行中)
HRLoveFun Sep 10, 2026
1d55b45
feat(ui): B7 模块级参数——toolbar 持有、store 记忆、只重跑消费它的模块
HRLoveFun Sep 10, 2026
e026c67
chore(ui): B8 退役 Config 页签(§8 Q2 = 删除)
HRLoveFun Sep 10, 2026
9691776
docs(reorg): B1–B8 验收评审 + F6 文档陈旧项修复
HRLoveFun Sep 10, 2026
2ab26cf
fix(reorg): B9 整改 F1–F3 —— 模块参数 memo / 参数栏收起 / 图标
HRLoveFun Sep 10, 2026
e32e2b9
docs(reorg): B9 batch note —— e2e 全量 exit 0
HRLoveFun Sep 10, 2026
fea0e54
fix(reorg): B9 二轮 —— F4 feature_bars 自愈 + F5-b/c 陈旧 UI 清理
HRLoveFun Sep 10, 2026
aca2ca1
refactor(reorg): B9 收尾 —— F5-a 退役期权叠加图 + 删除 summary.py 死代码
HRLoveFun Sep 10, 2026
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
10 changes: 5 additions & 5 deletions .github/agents/pipeline-doctor.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ You are a data pipeline diagnostician for the OptionView project. Your job is to
## Architecture

```
data_pipeline/downloader.py → raw_prices table
data_pipeline/cleaning.py → clean_prices table
data_pipeline/processing.py → processed_prices table
data_pipeline/ingest/ohlcv.py → raw_bars table
data_pipeline/transform/cleaning.py → clean_bars table
data_pipeline/transform/processing.py → feature_bars table
core/price_dynamic.py → features DataFrame
core/market_analyzer.py → chart generation
services/market/analysis/facade.py → base64 images to frontend
Expand All @@ -26,8 +26,8 @@ services/market/analysis/facade.py → base64 images to frontend
## Approach

1. **Clarify symptom**: What's the user seeing? Empty chart, wrong data, error message?
2. **Check DB tables** (raw_pricesclean_pricesprocessed_prices) for the target ticker
3. **Look for NaN-only rows**: `SELECT count(*) FROM raw_prices WHERE ticker=? AND open IS NULL AND close IS NULL`
2. **Check DB tables** (raw_barsclean_barsfeature_bars) for the target ticker
3. **Look for NaN-only rows**: `SELECT count(*) FROM raw_bars WHERE ticker=? AND open IS NULL AND close IS NULL`
4. **Check logs**: Look for yfinance errors (429, timeout), "No new data", pipeline warnings
5. **Trace the failure**: Which stage first produced invalid data? Follow downstream
6. **Check connectivity**: If download is suspected, verify proxy and throttle state
Expand Down
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Flask-based market analysis dashboard with options strategy tools.

## Database

- SQLite via `data_pipeline/db.py` — always use `get_conn()` context manager
- SQLite via `data_pipeline/store/db.py` — always use `get_conn()` context manager
- WAL mode enabled; `PRAGMA synchronous=NORMAL`
- DB path from `MARKET_DB_PATH` env var, default `./market_data.sqlite`

Expand Down Expand Up @@ -59,7 +59,7 @@ gunicorn app:app -b 0.0.0.0:5000 # production

Before suggesting non-trivial changes, consult these:

- **[docs/plans/business_line_reorg.md](../docs/plans/business_line_reorg.md)** — **active reorg** of `data_pipeline/`, the parameter surfaces, and `routes/core.py::index`. ADRs [0011](../docs/decisions/0011-pluggable-data-provider-seam.md) / [0012](../docs/decisions/0012-parameter-ownership-and-prefetch.md) are **Accepted**. Read §0 first: work batches B1–B8 in order, one batch per PR, update the ledger in the same commit, don't re-litigate the Accepted ADRs.
- **[docs/plans/business_line_reorg.md](../docs/plans/business_line_reorg.md)** — the 2026-09 business-line reorg (provider seam + canonical schema, ticker-only Parameters bar, readiness prefetch). Batches **B1–B9 landed**; §10 lists the deferred follow-ups. ADRs [0011](../docs/decisions/0011-pluggable-data-provider-seam.md) / [0012](../docs/decisions/0012-parameter-ownership-and-prefetch.md) are **Accepted**don't re-litigate.
- **[docs/constraints.md](../docs/constraints.md)** — external/historical constraints (yfinance limits, SQLite choice, single-machine assumption, intentional "magic numbers"). Read this before flagging anything as tech debt.
- **[docs/glossary.md](../docs/glossary.md)** — domain terms (IV vs HV, Greeks, regime, anomaly flags). Read this before assuming a term means what you think it means.
- **[docs/decisions/](../docs/decisions/)** — Architecture Decision Records. Each ADR explains the context, options considered, and accepted trade-offs for a major design choice.
Expand Down
2 changes: 1 addition & 1 deletion .github/data/arch_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"layer_violations": 0,
"cycles": 0,
"god_files": 0,
"dead_code_candidates": 1
"dead_code_candidates": 0
}
14 changes: 7 additions & 7 deletions .github/data/failure-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ patterns:
resolved: false
resolution_note: null
related_files:
- data_pipeline/cleaning.py
- data_pipeline/downloader.py
- data_pipeline/transform/cleaning.py
- data_pipeline/ingest/ohlcv.py
- core/price_dynamic.py

empty-dataframe:
Expand All @@ -39,8 +39,8 @@ patterns:
resolved: false
resolution_note: null
related_files:
- data_pipeline/downloader.py
- data_pipeline/processing.py
- data_pipeline/ingest/ohlcv.py
- data_pipeline/transform/processing.py
- services/market/analysis/facade.py

dtype-mismatch:
Expand All @@ -53,7 +53,7 @@ patterns:
resolved: false
resolution_note: null
related_files:
- data_pipeline/db.py
- data_pipeline/store/db.py
- core/options_greeks.py
- core/market_analyzer.py

Expand All @@ -67,7 +67,7 @@ patterns:
resolved: false
resolution_note: null
related_files:
- data_pipeline/downloader.py
- data_pipeline/ingest/ohlcv.py
- utils/utils.py

db-error:
Expand All @@ -80,7 +80,7 @@ patterns:
resolved: false
resolution_note: null
related_files:
- data_pipeline/db.py
- data_pipeline/store/db.py

greeks-edge-case:
hook_regex: "greeks|black.?scholes|delta|gamma|theta|vega.*nan"
Expand Down
24 changes: 12 additions & 12 deletions .github/data/tag_baseline.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
{
"files_scanned": 136,
"files_scanned": 147,
"tags_by_type": {
"WHY": 12,
"CONSTRAINT": 16,
"TRADEOFF": 2,
"INVARIANT": 3,
"WHY": 13,
"CONSTRAINT": 19,
"TRADEOFF": 3,
"INVARIANT": 9,
"DOMAIN": 10,
"HACK": 0,
"WORKAROUND": 0
},
"uncovered_constants_count": 16,
"uncovered_constants": [
"data_pipeline/data_ops/_globals.py:14: _UPDATE_COOLDOWN=60",
"data_pipeline/data_ops/_globals.py:16: _QUERY_CACHE_TTL=60",
"data_pipeline/data_ops/_range.py:12: _ENSURE_RANGE_TTL=300",
"data_pipeline/data_ops/_range.py:19: _SENTINEL_GAP_THRESHOLD_DAYS=365",
"data_pipeline/data_ops/_range.py:20: _SENTINEL_MIN_DB_SPAN_DAYS=365",
"services/market/charts.py:30: _CACHE_MAX_ENTRIES=64",
"services/options/preload.py:32: CACHE_TTL_MINUTES=15",
"data_pipeline/_state.py:29: _UPDATE_COOLDOWN=60",
"data_pipeline/_state.py:31: _QUERY_CACHE_TTL=60",
"data_pipeline/orchestrate/backfill.py:12: _ENSURE_RANGE_TTL=300",
"data_pipeline/orchestrate/backfill.py:19: _SENTINEL_GAP_THRESHOLD_DAYS=365",
"data_pipeline/orchestrate/backfill.py:20: _SENTINEL_MIN_DB_SPAN_DAYS=365",
"services/market/charts.py:29: _CACHE_MAX_ENTRIES=64",
"services/options/preload.py:34: CACHE_TTL_MINUTES=15",
"services/options/simulation.py:36: MAX_STRIKES=15",
"services/options/simulation.py:37: MAX_EXPIRIES=6",
"services/options/simulation.py:38: MAX_IVS=5",
Expand Down
2 changes: 1 addition & 1 deletion .github/instructions/data-pipeline.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ applyTo: "data_pipeline/**"
# Data Pipeline Rules

## DB Access
- Always use `get_conn()` context manager from `data_pipeline/db.py` — never raw `sqlite3.connect()`
- Always use `get_conn()` context manager from `data_pipeline/store/db.py` — never raw `sqlite3.connect()`
- Use `fetch_df()` for reads, `upsert_many()` for writes
- Convert DB-sourced columns with `pd.to_numeric(col, errors='coerce')` before any math — SQLite returns `object` dtype

Expand Down
2 changes: 1 addition & 1 deletion .github/prompts/diagnose.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ argument-hint: "Ticker symbol or symptom (e.g., 'NVDA empty charts', 'stale TLT
Diagnose why a specific ticker's data is missing, stale, or showing errors in the OptionView dashboard.

Steps:
1. Check the DB for the ticker: query `raw_prices`, `clean_prices`, `processed_prices` for recent rows
1. Check the DB for the ticker: query `raw_bars`, `clean_bars`, `feature_bars` for recent rows
2. Look for NaN-only filler rows (root cause of empty charts)
3. Check yfinance download logs for errors (429, timeout)
4. Trace data flow through the 5-stage pipeline to find the failure point
Expand Down
2 changes: 1 addition & 1 deletion .github/prompts/new-test.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
description: "Generate a test following OptionView project patterns for a specific module or function."
agent: "agent"
tools: [read, search, edit]
argument-hint: "Module or function to test (e.g., 'data_pipeline/cleaning.py clean_range')"
argument-hint: "Module or function to test (e.g., 'data_pipeline/transform/cleaning.py clean_range')"
---

Generate a pytest test for the specified module/function following OptionView test conventions:
Expand Down
12 changes: 6 additions & 6 deletions .github/prompts/pipeline-status.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ Check the current health of the OptionView data pipeline:
1. Query the SQLite database (`market_data.sqlite`) for:
```sql
-- Row counts per table
SELECT 'raw_prices' as tbl, count(*) as rows FROM raw_prices
UNION SELECT 'clean_prices', count(*) FROM clean_prices
UNION SELECT 'processed_prices', count(*) FROM processed_prices;
SELECT 'raw_bars' as tbl, count(*) as rows FROM raw_bars
UNION SELECT 'clean_bars', count(*) FROM clean_bars
UNION SELECT 'feature_bars', count(*) FROM feature_bars;

-- Latest data per ticker
SELECT ticker, MAX(date) as latest, COUNT(*) as rows FROM raw_prices GROUP BY ticker;
SELECT ticker, MAX(date) as latest, COUNT(*) as rows FROM raw_bars GROUP BY ticker;

-- NaN-only filler rows (problematic)
SELECT ticker, count(*) as nan_rows FROM raw_prices
SELECT ticker, count(*) as nan_rows FROM raw_bars
WHERE open IS NULL AND high IS NULL AND low IS NULL AND close IS NULL
GROUP BY ticker HAVING nan_rows > 0;

-- Data freshness (days since last update)
SELECT ticker, MAX(date) as latest,
julianday('now') - julianday(MAX(date)) as days_stale
FROM raw_prices GROUP BY ticker ORDER BY days_stale DESC;
FROM raw_bars GROUP BY ticker ORDER BY days_stale DESC;
```

2. Report:
Expand Down
28 changes: 14 additions & 14 deletions .github/skills/debug-pipeline/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,26 @@ Classify the user's report:
| Symptom | Likely Layer |
|---------|-------------|
| Empty chart panels | core/ (PriceDynamic) or data_pipeline/ (NaN filler rows) |
| "No data for TICKER" message | data_pipeline/downloader.py (download failed) |
| "No data for TICKER" message | data_pipeline/ingest/ohlcv.py (download failed) |
| Stale prices (dates from days ago) | data_pipeline/data_service.py (cooldown blocking refresh) |
| 429 / timeout errors | yfinance rate-limiting or proxy issue |
| Wrong values in analysis | data_pipeline/cleaning.py or processing.py |
| Wrong values in analysis | data_pipeline/transform/cleaning.py or processing.py |

### Step 2: Check DB State

Query the database for the target ticker using the terminal:
```sql
-- Check raw_prices for recent data
SELECT ticker, date, close FROM raw_prices WHERE ticker='{TICKER}' ORDER BY date DESC LIMIT 5;
-- Check raw_bars for recent data
SELECT ticker, date, close FROM raw_bars WHERE ticker='{TICKER}' ORDER BY date DESC LIMIT 5;

-- Check for NaN-only filler rows (the root cause of empty charts)
SELECT count(*) FROM raw_prices WHERE ticker='{TICKER}' AND open IS NULL AND high IS NULL AND low IS NULL AND close IS NULL;
SELECT count(*) FROM raw_bars WHERE ticker='{TICKER}' AND open IS NULL AND high IS NULL AND low IS NULL AND close IS NULL;

-- Check clean_prices status
SELECT ticker, date, missing_any, price_jump_flag FROM clean_prices WHERE ticker='{TICKER}' ORDER BY date DESC LIMIT 5;
-- Check clean_bars status
SELECT ticker, date, missing_any, price_jump_flag FROM clean_bars WHERE ticker='{TICKER}' ORDER BY date DESC LIMIT 5;

-- Check processed_prices
SELECT ticker, date, frequency FROM processed_prices WHERE ticker='{TICKER}' ORDER BY date DESC LIMIT 5;
-- Check feature_bars
SELECT ticker, date, frequency FROM feature_bars WHERE ticker='{TICKER}' ORDER BY date DESC LIMIT 5;
```

### Step 3: Check yfinance Connectivity
Expand All @@ -66,10 +66,10 @@ print(df.tail() if not df.empty else "EMPTY - download failed")

Follow the data through each stage, checking for where it breaks. See [pipeline stages reference](./references/pipeline-stages.md) for expected inputs/outputs at each stage.

1. **downloader.py** → `upsert_raw_prices()` → writes to `raw_prices`
2. **cleaning.py** → `clean_range()` → reads `raw_prices`, writes to `clean_prices`
3. **processing.py** → `build_features()` → reads `clean_prices`, writes to `processed_prices`
4. **core/price_dynamic.py** → `_fetch_daily_from_db()` → reads `processed_prices`
1. **downloader.py** → `upsert_raw_prices()` → writes to `raw_bars`
2. **cleaning.py** → `clean_range()` → reads `raw_bars`, writes to `clean_bars`
3. **processing.py** → `build_features()` → reads `clean_bars`, writes to `feature_bars`
4. **core/price_dynamic.py** → `_fetch_daily_from_db()` → reads `feature_bars`
5. **core/market_analyzer.py** → uses PriceDynamic features for charts
6. **services/market/analysis/facade.py** → calls chart methods, returns base64 images

Expand All @@ -78,7 +78,7 @@ Follow the data through each stage, checking for where it breaks. See [pipeline
Common root causes:
| Root Cause | Evidence | Fix |
|-----------|----------|-----|
| NaN-only filler rows from failed download | `raw_prices` has NULL in all price columns | Re-download with `yf_throttle()`, delete filler rows |
| NaN-only filler rows from failed download | `raw_bars` has NULL in all price columns | Re-download with `yf_throttle()`, delete filler rows |
| 60s cooldown blocking retry | Download skipped, log says "No new data" | Wait 60s or reset cooldown in `DataService._ticker_locks` |
| Proxy unreachable | `curl: (28) Operation timed out` | Check `YF_PROXY` in `.env`, verify proxy is running |
| yfinance 429 rate limit | `YFRateLimitError` in logs | Wait 30s, ensure `yf_throttle()` is called everywhere |
Expand Down
26 changes: 13 additions & 13 deletions .github/skills/debug-pipeline/references/pipeline-stages.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,52 @@

Data flows through 5 stages. A failure at any stage can propagate downstream as empty/NaN data.

## Stage 1: Download (`data_pipeline/downloader.py`)
## Stage 1: Download (`data_pipeline/ingest/ohlcv.py`)

**Function**: `upsert_raw_prices(ticker, start, end)`
**Input**: Ticker symbol, date range
**Output**: `PipelineResult(ok=True, rows=N)` or `PipelineResult(ok=False, error="...")`
**Side effect**: Writes to `raw_prices` table
**Side effect**: Writes to `raw_bars` table

**What can fail**:
- yfinance returns empty DataFrame (rate-limit, invalid ticker, network error)
- Proxy unreachable (curl_cffi timeout)
- Staleness check incorrectly skips download

**Check**: `SELECT count(*) FROM raw_prices WHERE ticker=? AND date BETWEEN ? AND ?`
**Check**: `SELECT count(*) FROM raw_bars WHERE ticker=? AND date BETWEEN ? AND ?`

## Stage 2: Clean (`data_pipeline/cleaning.py`)
## Stage 2: Clean (`data_pipeline/transform/cleaning.py`)

**Function**: `clean_range(ticker, start, end)`
**Input**: Reads from `raw_prices` table
**Output**: `PipelineResult` — writes to `clean_prices` table
**Input**: Reads from `raw_bars` table
**Output**: `PipelineResult` — writes to `clean_bars` table
**Side effect**: Adds anomaly flags (price_jump_flag, vol_anom_flag, ohlc_inconsistent)

**What can fail**:
- Source `raw_prices` has NaN-only filler rows → cleans "pass through" NaN
- Source `raw_bars` has NaN-only filler rows → cleans "pass through" NaN
- `pd.to_numeric()` coerces strings to NaN silently
- Anomaly flag thresholds are heuristic — may miss or over-flag

**Check**: `SELECT date, missing_any, price_jump_flag FROM clean_prices WHERE ticker=? ORDER BY date DESC LIMIT 10`
**Check**: `SELECT date, missing_any, price_jump_flag FROM clean_bars WHERE ticker=? ORDER BY date DESC LIMIT 10`

## Stage 3: Process (`data_pipeline/processing.py`)
## Stage 3: Process (`data_pipeline/transform/processing.py`)

**Function**: `build_features(ticker, frequency)`
**Input**: Reads from `clean_prices` table
**Output**: `PipelineResult` — writes to `processed_prices` table
**Input**: Reads from `clean_bars` table
**Output**: `PipelineResult` — writes to `feature_bars` table
**Side effect**: Computes MA, returns, volatility features

**What can fail**:
- Insufficient clean data for rolling window calculations → features are NaN
- Wrong frequency conversion (D→W→M) drops rows
- `object` dtype from DB causes numpy math errors

**Check**: `SELECT date, frequency, ma_20, ma_50 FROM processed_prices WHERE ticker=? AND frequency=? ORDER BY date DESC LIMIT 5`
**Check**: `SELECT date, frequency, ma_20, ma_50 FROM feature_bars WHERE ticker=? AND frequency=? ORDER BY date DESC LIMIT 5`

## Stage 4: Core Analysis (`core/price_dynamic.py`, `core/market_analyzer.py`)

**Function**: `PriceDynamic._fetch_daily_from_db()` → `MarketAnalyzer` methods
**Input**: Reads from `processed_prices` (or `clean_prices` for some features)
**Input**: Reads from `feature_bars` (or `clean_bars` for some features)
**Output**: DataFrames for chart generation

**What can fail**:
Expand Down
2 changes: 1 addition & 1 deletion .github/skills/fix-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Verify the fix uses the correct error pattern for its layer:
For each changed production file, check that a corresponding test exists:
```bash
# Map production file to test file
# data_pipeline/downloader.py → tests/test_yf_download.py or tests/test_processing.py
# data_pipeline/ingest/ohlcv.py → tests/test_yf_download.py or tests/test_processing.py
# core/market_analyzer.py → tests/test_market_review.py
# services/market/validation.py → tests/test_validation.py
```
Expand Down
8 changes: 4 additions & 4 deletions .github/skills/test-escalation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_download_empty(mock_dl):
assert result.ok # Rows=0 is valid
assert result.rows == 0
# Verify no NaN filler rows were created
df = fetch_df("SELECT * FROM raw_prices WHERE ticker='NVDA'")
df = fetch_df("SELECT * FROM raw_bars WHERE ticker='NVDA'")
assert df.empty
```

Expand All @@ -78,16 +78,16 @@ def test_full_pipeline_with_nan_data(tmp_path, monkeypatch):

# Seed NaN-only filler rows (simulates failed download)
upsert_many(
"raw_prices",
"raw_bars",
["ticker", "date", "open", "high", "low", "close"],
[("NVDA", "2026-03-28", None, None, None, None)],
)

# Run cleaning — should NOT propagate NaN rows
result = clean_range("NVDA", dt.date(2026, 3, 28), dt.date(2026, 3, 28))

# Verify: clean_prices should be empty (NaN rows filtered)
df = fetch_df("SELECT * FROM clean_prices WHERE ticker='NVDA'")
# Verify: clean_bars should be empty (NaN rows filtered)
df = fetch_df("SELECT * FROM clean_bars WHERE ticker='NVDA'")
assert df.empty or df["close"].notna().all()
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Is the bug reproducible with a simple unit test?
- **Symptom**: Empty charts, 0 historical data points
- **Root cause**: NaN-only filler rows from failed download survive cleaning
- **Effective level**: Level 2 (integration — needs real DB to reproduce the chain)
- **Key assertion**: After pipeline, `processed_prices` has no NaN-only rows
- **Key assertion**: After pipeline, `feature_bars` has no NaN-only rows

### Pattern B: yfinance Silent Failure
- **Symptom**: Data appears stale, "No new data" in logs
Expand Down
Loading
Loading