From 1471d19e008af34646114d41c089f71f03a34013 Mon Sep 17 00:00:00 2001 From: GradientDescent Date: Fri, 11 Sep 2026 21:35:08 +0800 Subject: [PATCH] =?UTF-8?q?docs(futu):=20=E6=8C=89=E8=82=A1=E4=BB=B7/?= =?UTF-8?q?=E6=9C=9F=E6=9D=83=E6=95=B0=E6=8D=AE=E4=BE=9D=E8=B5=96=E7=BB=99?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=88=86=E7=B1=BB=20(=C2=A73.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回应"按股价 vs 期权数据划分模块是否有助于接入 futu、哪些模块两者都用、 futu 期权数据能否顺带给股价信息"的追问。grep 校验,非猜测: - 纯股价模块(Market Review/Statistical/Assessment/Regime/Simulation)完全 不碰 fetch_option_chain,futu 接入零影响。 - 纯期权模块(Option Chain/Payoff Ratio/candidate 矩阵/IV 系列图表)里的 spot 本来就是 fetch_option_chain() 内部顺带解析、塞进同一份快照的,不算 第二数据源;futu provider 做同样的事即可(§3.3 已有设计)。 - 两者都用的只有 2 处,且是同一种模式(HV 百分位 vs 期权 IV,ADR 0004 的 落地点):services/options/chain.py 的 Vol Premium 区块、 services/options/builder.py 的 _vol_context。两处历史价格调用和期权链 调用本就是两次独立调用,只在最后合并派生标量——接入 futu 不需要碰这两处 的历史价格那条腿。 - futu 解析 spot 时顺带拿到的标的报价比"仅最新价"丰富(今日开高低/昨收/ 成交量),但终究是快照不是序列,替代不了 HV 计算需要的 ~250 交易日历史, 这两个模块的股价腿仍然只能走 yfinance。 新增 §3.6,充实 §3.3/§4.1,TL;DR 加两行。无代码改动,无新增 ADR。 Co-Authored-By: Claude Sonnet 5 --- docs/plans/futu_provider_integration.md | 83 ++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/docs/plans/futu_provider_integration.md b/docs/plans/futu_provider_integration.md index f616ffe..634c697 100644 --- a/docs/plans/futu_provider_integration.md +++ b/docs/plans/futu_provider_integration.md @@ -1,7 +1,8 @@ # Exploration: futu-api as a Second Data Provider -**Date**: 2026-09-11, updated 2026-09-11 (Part F + mainstream-vs-plan pass) | -**Owner**: repo owner | **Status**: EXPLORATION — no futu code, no new ADR +**Date**: 2026-09-11, updated 2026-09-11 (Part F + mainstream-vs-plan pass; +§3.6 module-split pass) | **Owner**: repo owner | **Status**: EXPLORATION — +no futu code, no new ADR > **Update**: the business-line reorg's batches **B1–B10 have since landed** > (merged to `main`, see `business_line_reorg.md` §0/§10) — `providers/base.py`, @@ -34,6 +35,8 @@ current codebase and the now-landed ADR 0011 canonical schema. | How does it map to our format? | Two live shapes: the canonical `OptionChainSnapshot`/`OptionLeg` dataclasses (`providers/base.py`, landed but currently uncalled), and the legacy `{ticker, spot, expiries, chain{exp:{calls:DataFrame, puts:DataFrame}}}` dict every real consumer still uses. Mapping is 1:1 either way except: IV `/= 100`, `inTheMoney`/`in_the_money` computed from strike vs spot, `bid`/`ask` left empty. Ticker conversion already exists (`utils/ticker_utils.py`). A futu provider should target the canonical shape and migrate the consumers — see §3.1. | | How to avoid contention with yfinance? | Four seams: (1) `provider` column as a real DB discriminator; (2) a **separate** throttle — never share `yf_throttle()`; (3) explicit provider selection (env default + per-market routing), not "whoever answers first"; (4) live option data is not persisted (ADR 0004), so cache-key by `(ticker, provider)` and there is no row-level race. | | Biggest blocker | **OpenD is a stateful, authenticated, GUI-oriented local daemon.** It breaks ADR 0002's "works out of the box / zero infra" property and cannot be dropped onto a headless VPS without the Linux headless build + a login ceremony + crash supervision. | +| Which modules need both stock price *and* option data? | Only **two call sites**, both the same pattern (HV percentile from ~1y of daily closes vs. current option IV, per ADR 0004): the Volatility Analysis tab's "Vol Premium" block and the Strategy Builder's `vol_context`. Everywhere else is cleanly price-only or option-only already — see §3.6. | +| Can futu's option data also give us stock price info (e.g. latest price)? | The underlying's `subscribe`+`get_stock_quote` call (already needed to resolve `spot`, §3.3) returns a full current-session quote — last price, today's OHLC, prev close, volume — not just "last price". But it's a **snapshot, not a series**, so it cannot feed the two "both" modules' HV calc; their price leg stays on yfinance regardless. | --- @@ -291,6 +294,17 @@ market hours" is not a fix. Resolution order for a futu provider: instead of crashing (the archived code chose this; the analyzer contract would need a small softening — a follow-up, not blocking). +**This resolution step is richer than "just a price".** Subscribing to the +*underlying* stock code (not an option contract) through the same +`subscribe`+`get_stock_quote` call returns a full current-session quote — +`last_price`, `open_price`/`high_price`/`low_price` (today's OHLC), +`prev_close_price`, `volume`, `turnover`, `turnover_rate`, `amplitude` — not +merely the last trade. That's enough for any *point-in-time* need (display +spot, moneyness, ITM classification, "today's range"). It is **still only a +snapshot**, not a time series, so it cannot feed anything that needs history +— see §3.6 for exactly which modules that rules out, and why it doesn't +matter for most of them. + ### 3.4 OHLCV mapping (recommendation: don't) Futu history (`request_history_kline`) is heavily quota-limited for non-paying @@ -310,6 +324,58 @@ keep the yahoo form. A futu provider consumes the futu form from the same tuple. Futures (`GC=F`) and most `^`-indices have **no** futu equivalent — the provider must declare which symbols it can serve (see §4.2). +### 3.6 Module split: price-only / option-only / both + +Added 2026-09-11, answering "given options move to futu and OHLCV stays on +yfinance, does splitting modules by data dependency help, and which modules +need both?" This is a grep-verified classification of the current tree, not a +guess — see the actual import/call sites cited per row. + +| Split | Modules | Why this matters for the futu swap | +|---|---|---| +| **Price-only** | Market Review, Statistical Analysis, Assessment & Projections, Market Regime, Simulation (spot lookup only) | Never touch `fetch_option_chain`/`OptionsChainAnalyzer`. Zero exposure to an option-provider swap — this is most of the app. | +| **Option-only** | Option Chain tab, Payoff Ratio, `core/decision/candidate.py` (delta/DTE candidate matrix), `core/decision/market_data.py` (IV rank/percentile **across the chain's own term structure**, not across time — not an ADR 0004 violation), the IV-smile/surface/skew/term-structure/PCR/OI charts | `spot` here is **not** a second data source — `fetch_option_chain()` resolves it internally and bundles it into the one snapshot dict (today via yfinance's own `fetch_spot`; a futu provider does the equivalent per §3.3). One provider call in, one snapshot out; a futu swap is a same-shape, local change. | +| **Both — exactly 2 call sites** | ① `services/options/chain.py::generate_options_chain_analysis` (Volatility Analysis tab's "Vol Premium" block); ② `services/options/builder.py::_vol_context` (Strategy Builder) | See below. | + +**The two "both" sites, precisely:** + +``` +① services/options/chain.py L230-244 (feeds oc_vol_premium) + build_data_context(ticker, 365d, "D") ← yfinance/DataService, a daily-bar series + + atm_iv from analyzer.chain[nearest]["puts"] ← option chain, one snapshot value + → core.signals.hv.vol_premium_context(close_series, atm_iv) + +② services/options/builder.py L94-136 (feeds the built strategy's vol_context) + DataService.get_cleaned_daily(ticker, 400d) ← yfinance/DataService, a daily-bar series + + avg_iv_pct across the built legs ← option chain, snapshot values + → hv_pct / hv_percentile → cheap/fair/rich label +``` + +Both are the same shape: HV percentile (needs **≥250 trading days** of daily +closes) compared against current option IV — the ADR 0004 "HV percentile +substitutes for IV rank" pattern is, concretely, the one place in the app +where a price series and an option snapshot must both be present at once. + +**Why the split helps, and what it doesn't fix:** + +- **It already exists at the code level.** Both call sites fetch price-history + and option-chain through two independent function calls and merge only the + *derived scalars* (an HV-percentile number, an IV-percent number) — never a + shared DataFrame or a shared fetch. Swapping the option side to futu touches + neither call's price leg; `DataService.get_cleaned_daily` / + `build_data_context` keep running against yfinance unchanged. No new + plumbing is needed to keep these two decoupled — don't introduce any. +- **futu's underlying-quote snapshot (§3.3) cannot close this gap.** HV needs a + time series; a richer point-in-time quote (even with today's OHLC) is still + one data point. The price leg of ①② must stay on yfinance regardless of + which provider serves the option leg — "futu gives us the latest price too" + does not let these two modules drop their yfinance dependency. +- **One thing to actively avoid**: a future refactor "simplifying" ①/② by + reusing the spot/OHLC that a futu `option_chain()` call already fetched, + instead of a separate `get_cleaned_daily` call. That would silently swap a + 252-day history for a single day's bar and produce a nonsensical HV number. + Keep the two fetches structurally separate, as they are today. + --- ## 4. Part D — Preventing contention with yfinance @@ -344,6 +410,13 @@ Routing table (proposed): The value futu actually adds is **HK/CN options** and **Greeks** — not competing with yfinance on US equities. +This table's "OHLCV history" and "Spot" rows are not routing choices in the +usual sense — per §3.6, every module that touches historical prices +(price-only *and* the price leg of the two "both" modules) is hard-wired to +yfinance/`DataService` regardless of the option-side provider; there is no +symbol class where futu's history/spot would be *selected*. The routing that +actually matters is the "Option chain" row alone. + ### 4.2 `MarketDataProvider` protocol must not be yfinance-shaped This is now the real, landed `data_pipeline/providers/base.py` — see the exact @@ -508,6 +581,12 @@ in place. §3.1): `core/options/chain/analyzer.py::OptionsChainAnalyzer`, `services/options/chain.py`, `services/options/preload.py`, `services/options/builder.py`, `core/decision/candidate.py` +- Module split (§3.6) — the two price+option merge points: + `services/options/chain.py::generate_options_chain_analysis` (L230-244), + `services/options/builder.py::_vol_context` (L94-136), + `core/signals/hv.py` (`hv_pct`, `hv_percentile`, `vol_premium_context`), + `core/decision/market_data.py` (option-only: term-structure IV rank, not a + time-history metric — not the ADR 0004 kind) - Ticker conversion: `utils/ticker_utils.py` (`normalize_ticker`, `yahoo_to_futu`) - Seam target: [ADR 0011](../decisions/0011-pluggable-data-provider-seam.md), [ADR 0012](../decisions/0012-parameter-ownership-and-prefetch.md),