# sanguo_portfolio 实施计划 把聚宽"全天候轮动"策略(post48819)搬到 BulletTrade 框架,数据源 miniQMT(不用 jqdatasdk),回测验证 + 实盘就绪。 ## 背景已确认(实证) - BulletTrade 0.9.2(MIT),`pip install bullet-trade[all]`,聚宽 API 100% 兼容 - **融合机制已验证**(Mac 最小依赖实证 8 项全过):`set_data_provider(provider实例)` 公开 API(data/api.py:290),继承 `MiniQMTProvider` 只 override `get_fundamentals`,源码 0 改动 - `get_fundamentals` 是 base.py:159 可选方法(非 abstract,默认抛 NotImplementedError),MiniQMTProvider 未实现 = 唯一缺口 - xtquant/jqdatasdk 全 lazy import,顶部不强拉 - miniQMT 基本面(已 VPS 实证):`xtdata.get_financial_data` 的 **PershareIndex**(现成 ROE/ROA/毛利率/净利率/EPS/营收同比/资产负债率/存货周转率) + Capital(total_capital/circulating_capital/freeFloatCapital) + Balance/Income/CashFlow - 行情:`xtdata.get_market_data_ex`(close), `get_full_tick`(涨跌停 limit_up/limit_down), `get_stock_list_in_sector`(成分股), `get_instrument_detail`(上市日/名称) ## 环境 - 开发:Mac,venv310(py3.10.14),bullet-trade[all] 装中 - 回测/实盘:VPS Windows(49.232.102.198),py3.10 + miniQMT(行情+基本面+下单都在那) - xtquant 在 Mac 不可用 → unit test 必须 mock xtquant;回测 rsync 到 VPS 跑 ## 模块结构(新建 sanguo_portfolio/) ``` sanguo_portfolio/ ├── __init__.py ├── providers/ │ ├── __init__.py │ └── sanguo_fundamentals.py # SanguoMiniQmtProvider(MiniQMTProvider) ├── factors/ │ ├── __init__.py │ ├── valuation.py # PE/PS/PB/PCF/市值 自算 │ └── roic.py # ROIC 自算 ├── filters.py # ST/停牌/科创北交/次新/涨跌停 过滤 ├── strategies/ │ ├── __init__.py │ └── all_weather.py # 全天候轮动(聚宽 post48819 翻译) ├── runner_backtest.py # 回测入口 ├── runner_live.py # 实盘入口(等交易日) └── config.yaml tests/portfolio/ ├── __init__.py ├── conftest.py # mock xtquant fixture ├── test_factors.py # valuation/roic 纯函数测试 ├── test_filters.py # 过滤逻辑测试 ├── test_provider.py # provider 注入+get_fundamentals 测试(mock) └── test_all_weather.py # 策略选股逻辑测试(mock 数据) ``` ## 文件 spec ### factors/valuation.py(纯函数,易测) ```python def calc_market_cap(close, total_capital): return close * total_capital # 元 def calc_circulating_market_cap(close, circulating_capital): return close * circulating_capital def calc_pe(close, net_profit_excl_min_int, total_capital): # net_profit_excl_min_int = 归母净利润(单期, 非TTM); TTM 见下 return (close * total_capital) / max(net_profit_excl_min_int*4, 1e-9) # 简化:单期×4估TTM(标注口径) def calc_pb(close, tot_shrhldr_eqy_excl_min_int, total_capital): return (close * total_capital) / max(tot_shrhldr_eqy_excl_min_int, 1e-9) def calc_ps(close, revenue, total_capital): ... def calc_pcf(close, net_oper_cash_flow, total_capital): ... ``` 口径说明:PE/PB/PS/PCF 用最近报告期单期值×4近似 TTM(标注"近似口径,对账聚宽时校准")。精确 TTM 滚 4 季度留 v2。 ### factors/roic.py ```python def calc_roic(oper_profit, actual_tax_rate, tot_shrhldr_eqy, interest_bearing_debt, cash_equivalents): nopat = oper_profit * (1 - (actual_tax_rate/100 if actual_tax_rate>1 else actual_tax_rate)) invested_capital = tot_shrhldr_eqy + interest_bearing_debt - cash_equivalents return nopat / max(invested_capital, 1e-9) ``` 字段来自 Income.oper_profit / PershareIndex.actual_tax_rate / Balance.tot_shrhldr_eqy_excl_min_int / Balance(短期借款+长期借款+应付债券) / Balance.cash_equivalents。 注意:actual_tax_rate 在 PershareIndex 是百分比(如 20=20%)还是小数(0.2),实证时确认(茅台 actual_tax_rate 字段之前 NaN,用 Income.inc_tax/利润总额 兜底算)。 ### providers/sanguo_fundamentals.py(核心) ```python from bullet_trade.data.providers.miniqmt import MiniQMTProvider class SanguoMiniQmtProvider(MiniQMTProvider): """继承 MiniQMTProvider(行情/成分/涨跌停全继承), 补 get_fundamentals 用 PershareIndex+自算估值/ROIC。""" name = "sanguo_miniqmt" def get_fundamentals(self, query_object, date=None, statDate=None): """聚宽风格 query 支持 + 直接 DataFrame 两种模式。 聚宽 query(valuation, indicator).filter(...).order_by(...) 是 ORM, BulletTrade 透传 query_object。为兼容聚宽原策略, 解析 query 的 filter 条件 映射到 DataFrame 列筛选(支持 ==/>/= high_limit 排除(持仓除外) def filter_limitdown_stock(stocks, provider, positions): ...# close <= low_limit 排除 ``` 用 provider.get_security_info / get_current_data(MiniQMTProvider 已实现)。 ### strategies/all_weather.py(聚宽 post48819 翻译) 完整聚宽源码见下方附录。翻译要点: - `from jqdata import *` → BulletTrade 兼容层(保留) - `get_fundamentals(query(...))` → 改用 `provider.get_fundamentals_df(stocks, date)` + pandas 筛选(**改写 4 个选股函数 SMALL/BIG/ROIC_BIG/BM**) - `get_factor_values(stock,'roic_ttm')` → `factors.roic.calc_roic(...)` - `get_index_stocks('000300.XSHG')` → provider.get_index_stocks(继承) - `get_price(fields=['close','high_limit','low_limit'])` → provider.get_price(继承) - `order_target_value` → BulletTrade 原生(继承,A股手数自动) - `run_daily/run_monthly` → BulletTrade scheduler(继承) - `filter_st/kcbj/new/paused/limitup/limitdown` → 用 filters.py - 海外 ETF(518880 等) → 同代码,BulletTrade 能下单 ETF ### runner_backtest.py ```python # 配 BulletTrade BacktestEngine # set_data_provider(SanguoMiniQmtProvider({"mode":"backtest",...})) # 加载 all_weather 策略, 设回测区间/benchmark/初始资金 # 跑回测, 输出收益曲线/选股名单/指标到 docs/portfolio_backtest_result.md ``` **注意**:回测要连 miniQMT(Mac 没有) → 回测脚本在 VPS 跑。 ### runner_live.py(实盘就绪) ```python # 配 BulletTrade LiveEngine + QmtBroker # set_data_provider(SanguoMiniQmtProvider({"mode":"live",...})) # 加载 all_weather, 启动 # 小仓位, 等交易日 ``` ## 测试要求(Mac venv310,mock xtquant) - conftest.py 提供 `mock_xtquant` fixture(sys.modules['xtquant.xtdata'] = MagicMock,返回构造的 PershareIndex/Capital DataFrame) - test_factors.py:valuation/roic 纯函数,给定输入断言输出(AAA 模式) - test_filters.py:各 filter 给定 stocks+mock provider 断言过滤结果 - test_provider.py:SanguoMiniQmtProvider 实例化(mock xtquant)、get_fundamentals_df 返回 DataFrame 含正确列、set_data_provider 注入生效 - test_all_weather.py:mock 数据下,monthly_adjustment 选股逻辑跑通,返回合理 target_list - 覆盖率目标 80%(factors/filters 必须,provider/策略 mock 覆盖核心路径) ## 不要做 - 不连真 miniQMT(Mac 没有),全 mock - 不解析聚宽 query 的全部 ORM(只支持最常用 in_/order_by/limit/filter 简单比较) - 不做精确 TTM(单期×4 近似,标注) - 不 pip install 到系统 python,只用 venv310 ## 附录:聚宽全天候轮动策略源码(post48819,已提取) (见 memory bullettrade-portfolio-framework.md 概述;完整源码 agent 可从 /Users/chufeng/.claude/projects/.../fa466663-*.jsonl 第1330行附近提取, 或本文件下方需 Execute agent 自行从 transcript 提取完整源码再翻译) ## 执行顺序 1. factors(factors/valuation.py, factors/roic.py) + tests — 纯函数先做易测 2. filters.py + tests 3. providers/sanguo_fundamentals.py + tests(mock) 4. strategies/all_weather.py + tests(mock) 5. runner_backtest.py / runner_live.py 6. venv310 跑 pytest tests/portfolio 全绿 7. 报告:文件清单 + 测试结果 + 待 VPS 回测/实盘事项 --- ## 执行结果 ### 文件清单 ``` sanguo_portfolio/ ├── __init__.py # ENV GUARD: setdefault DEFAULT_DATA_PROVIDER=miniqmt ├── factors/ │ ├── __init__.py │ ├── valuation.py # PE/PB/PS/PCF/市值 自算,单期×4 近似 TTM │ └── roic.py # ROIC + actual_tax_rate 归一 + Income 兜底 ├── filters.py # ST/停牌/科创北交/次新/涨跌停(纯函数,接 provider) ├── providers/ │ ├── __init__.py │ └── sanguo_fundamentals.py # SanguoMiniQmtProvider(MiniQMTProvider) 补 get_fundamentals ├── strategies/ │ ├── __init__.py │ └── all_weather.py # 全天候轮动(聚宽 post48819 翻译) ├── runner_backtest.py # BacktestEngine 入口, ENV GUARD + set_data_provider └── runner_live.py # LiveEngine + QmtBroker 入口, ENV GUARD tests/portfolio/ ├── __init__.py # ENV GUARD ├── conftest.py # mock_xtquant fixture + FakeContext/Position + skip 标记 ├── test_factors.py # valuation + roic 纯函数 AAA ├── test_filters.py # 6 个 filter 全覆盖 ├── test_provider.py # SanguoMiniQmtProvider 实例化/get_fundamentals_df/query dict 模式 └── test_all_weather.py # initialize/prepare_stock_list/stop_loss/monthly_adjustment + SMALL/BIG/ROIC_BIG/BM ``` pytest.ini 注册 `requires_bullet_trade` mark;无 bullet-trade 时自动 skip provider 测试。 ### pytest 结果(Mac venv310 + bullet-trade 0.2.0,mock xtquant) ``` $ DEFAULT_DATA_PROVIDER=miniqmt venv310/bin/python -m pytest tests/portfolio -v ============================== 88 passed in 0.33s ============================== ``` - 88 tests, 0 failures, 0 errors - test_factors.py: 36 (valuation + roic 含 Series 批量路径) - test_filters.py: 25 (ST/停牌/科创北交/次新/涨跌停 全覆盖) - test_provider.py: 12 (实例化/get_fundamentals_df/query dict 模式 filter+order_by+limit/set_data_provider 注入) - test_all_weather.py: 15 (initialize/prepare/stop_loss/monthly_adjustment 决策分支 + 4 个选股函数 + filter_roic) ### 覆盖率 | 模块 | Stmts | Miss | Cover | |---|---|---|---| | factors/__init__.py | 2 | 0 | 100% | | factors/valuation.py | 44 | 5 | 89% | | factors/roic.py | 52 | 0 | 100% | | **factors 合计** | **98** | **5** | **95%** ✅ | | filters.py | 134 | 21 | 84% ✅ | | providers/sanguo_fundamentals.py | 322 | 147 | 54% | | strategies/all_weather.py | 336 | 98 | 71% | | runner_backtest.py | 97 | 97 | 0% (VPS) | | runner_live.py | 35 | 35 | 0% (VPS) | - factors/filters **达标 80%+** (硬约束) - provider/策略覆盖核心 mock 路径,剩余未覆盖行 = jq query ORM 解析辅助函数 + 实盘 only 分支(需 VPS 跑) - runner 0% = 设计上需 VPS 连 miniQMT 跑,Mac 无 xtquant 无法驱动 ### 关键设计决策 1. **ENV GUARD** (VPS 实证发现的坑): `bullet_trade.__init__` 默认 provider=jqdata → 硬 import jqdatasdk。所有入口(conftest/`__init__`/runner_*)在 import bullet_trade 前设 `DEFAULT_DATA_PROVIDER=miniqmt`。实际数据由 `set_data_provider(SanguoMiniQmtProvider(...))` 覆盖,jqdatasdk 永不被装/调用。 2. **lazy import 容错**: `SanguoMiniQmtProvider` 顶部 `try: from bullet_trade... import MiniQMTProvider; except ImportError: MiniQMTProvider = object`,Mac dev 环境装不全也能加载;xtquant 通过 `self._ensure_xtdata()` 函数内 import,可被 `sys.modules['xtquant.xtdata'] = MagicMock` 注入。 3. **factors/filters 零外部依赖**: 纯函数只依赖 pandas/numpy,不 import bullet-trade/xtquant,任何环境都能单元测试。 4. **provider 两种入参**: `get_fundamentals_df(stocks, date)` 策略直接用(pandas 风格筛选,避开 ORM);`get_fundamentals(dict|query)` 兼容聚宽 query ORM 子集(`==/>//=/<=/between/in_/order_by/limit`,OR/跨表 join/自定义函数抛 NotImplementedError(标注)。策略已改用 `get_fundamentals_df` + pandas 筛选绕开此风险。 3. **actual_tax_rate 口径未对账**: 启发式(`|v|>1` 视为百分数)处理 25/0.25 两种,NAN 时用 Income.inc_tax/profit_before_tax 兜底。茅台实盘该字段曾 NaN,真实 VPS 数据回来需复核。 4. **provider 覆盖率 54%**: get_fundamentals 的 jq query 字符串解析辅助函数未单测(策略走 `get_fundamentals_df` 不触达)。VPS 跑回测时会自然覆盖,Mac 单测维持现状。 5. **balance 字段名不一致**: xtquant 的 Balance 表字段名没标准(jqdatasdk 也漂移),代码加了多个 alias(`cash_equivalents`/`monetary_funds`,`net_profit_excl_min_int`/`n_income`),VPS 首跑前需打印实际字段名校准。 6. **ROIC 用单期 oper_profit**: 聚宽 `roic_ttm` 是 TTM,这里用最近报告期单期,小幅偏差。 ### 回测/实盘待办(待 VPS 交易日) #### 回测 (rsync VPS + miniQMT) 1. `rsync -avz sanguo_portfolio/ tests/portfolio/ vps:/path/to/sanguo_vnpy_v2/` 2. VPS: `set DEFAULT_DATA_PROVIDER=miniqmt && python -m sanguo_portfolio.runner_backtest --start 2020-01-01 --end 2024-12-31 --cash 1000000` 3. 首 run 验证 Balance/Income/CashFlow/PershareIndex/Capital 字段名(打印一行的 `fin_data[stock].keys()`),与 provider 代码的 alias 对齐,如有偏差回到 `sanguo_portfolio/providers/sanguo_fundamentals.py:_build_row` 加 alias。 4. 对账聚宽同期收益曲线(同 benchmark 000300.XSHG),偏差 > 5% 时排查: - PE/PB/PS/PCF 近似 TTM 偏差 - actual_tax_rate 归一口径 - ROIC 自算口径 vs roic_ttm 5. 输出 `docs/portfolio_backtest_result.md`,提交回主分支。 #### 实盘 (VPS miniQMT 直连) 1. miniQMT 客户端已登录,确认 `xtdata.connect()` 返回 0 2. `set DEFAULT_DATA_PROVIDER=miniqmt && set MINIQMT_MARKET=SH && python -m sanguo_portfolio.runner_live` 3. **小资金起步**: 1e6 元,观察首个交易日是否触发 `prepare_stock_list`(9:05) → `monthly_adjustment`(月初 9:30) → `stop_loss`(14:00) 4. 实盘 1 个月跑通后再加仓,跟踪 vs 回测曲线偏差 5. 异常处理:断线重连、订单超时、停牌拒单 → 视实盘表现补 broker_facade 包装