Files
sanguo_vnpy_v2/docs/design/multi-strategy-instance-budget-spec.md
T

8.9 KiB
Raw Blame History

Spec:多策略共享账户 × 实例预算制(定稿 2026-08-19

用户确认的最终方案。背景调研与决策过程见 memory multi-strategy-shared-account-plan。 本文是实施规格:A 部分是策略 session 的契约,B 部分是前后端 session 的实施项,C 部分是全删重建 runbook。 原则:模拟盘(影子+实走)零改动(每实例独立虚拟账户=参照系);实盘共享期上实例预算制;绑专属账号后本机制自然退化,不需要改回。

0. 架构一句话

策略实例 ──看到──> 实例账本(预算N万,归因自己的成交) ──下单──> 共享QMT账户
                                                      │
                                    supervisor 账户监视器 ──写──> 全局账户快照(现金/持仓,一条)
                                                      │
                                    预算校验(新建/修改时): Σ预算 ≤ 账户现金

A. 策略 session 交付(按本节完成即可,不需读 B/C)

A1. 卖出只卖自己(P0,最先做)

规则(magic number 铁律):调仓/轮换/清仓时,卖出目标集必须来自本实例持仓视图,禁止context.portfolio.positions(共享账户全量)取卖出名单。channel_test 现行"卖掉所有不在目标里的持仓"是反模式,必须改。

通道(已上线,dae56e2

# broker = 注入的 BrokerFacade(回测/实盘同构)
pos = getattr(self.broker, "get_instance_positions", None)
if pos is not None:
    my_positions = pos()   # 实盘:本实例账本视图
else:
    my_positions = {s: ... for s in context.portfolio.positions}  # 回测/测试回退原逻辑

返回契约Dict[symbol, {"amount": int, # 总持仓 "closeable_amount": int, # T+1 可卖(已扣当日买入) "avg_cost": float}]

  • 需要现价用 provider 查(视图不含 price,避免快照时点错位)。
  • 视图为空 dict = 本实例无持仓 → 轮换语义退化为"只买不卖"(这是正确行为,不是 bug)。

涉及策略channel_testrotate/swap_one/t1_probe 的卖出腿)、momentum/_ex(熊市清仓段)、small_cap/_ex_rebalance 清仓段)、all_weather/_ex(调仓卖出段)、value_selection/_ex。逐个过一遍卖出路径。

验收:影子日志次日 9:35 轮换只卖自己账本内的票;单测覆盖"实例视图为空→零卖出"。

A2. context 属性清单(P0,给前后端做虚拟化的输入)

盘点所有策略从 context 上读的属性,逐个列出(尤其 context.portfolio.*):

属性 哪些策略用 用途
portfolio.positions 卖出名单/持仓判断
portfolio.total_value 定寸(每只买多少)
portfolio.available_cash 资金判断
portfolio.positions_value / locked_cash / 其他
current_dt / previous_date / 其他非 portfolio 属性

输出就是这张表(补全真实值)。前后端用它实现 InstancePortfolio 代理——清单里没有的属性默认透传真 context,不会漏;清单的意义是把要覆盖的 portfolio 子属性做全,以及确认除 portfolio 外还有没有需要实例化的东西。

验收:表格发给前后端 session(写入 Gitea issue 或记忆均可)。

A3. 配合定寸虚拟化联调(P1,在 A1/A2 之后)

前后端完成 B2 后:策略代码不改(虚拟化对策略透明),但需配合验证——channel_test 实盘每只买入应回到 预算/6(虚拟化前是全账户/6)。异常情况(策略读到了没覆盖的属性)反馈前后端。


B. 前后端 session 交付

B1. supervisor 账户监视器 + 全局快照(B3/B4 的地基)

  • supervisorsanguo_live)起一个账户监视线程:独立 probe 连接(session id 专用的 int),60s 查一次 QMT 账户资金+持仓,upsert 一条全局快照(不再挂任何实例名下)。
  • 存储:新表 qmt_account_snapshot(按 QMT 账号一行):
CREATE TABLE IF NOT EXISTS qmt_account_snapshot (
  account TEXT PRIMARY KEY, cash REAL, market_value REAL, total REAL,
  positions TEXT,            -- 全账户持仓 JSON [{symbol,volume,can_use,avg_price,mv}]
  updated_at TEXT
);
  • 快照不可用/过期(>10 分钟)时,预算校验fail-closed(见 B3)。
  • 监视器与子进程解耦:零实盘实例时也照常运行(删光重建期间校验数据不能断)。

B2. 定寸虚拟化(InstancePortfolio 代理)

  • 位置:live_strategy._setup,包装经 facade 注册的回调——wrapper 收到真 context 后,把 context 换成代理再调策略函数;代理对除 portfolio 外的一切属性透传。
  • InstancePortfolio(每次调用现算):
    • available_cash = 实例账本 cash
    • total_value = 账本 equity(现价取真 portfolio 同标的现价,缺则加权成本)
    • positions = 账本视图的 jq 风格对象(属性名以 A2 清单为准,至少 total_amount/closeable_amount/avg_cost
    • 清单里的其余 portfolio 子属性按语义映射,账本没有的(如 locked_cash)给安全值
  • 引擎内部(订单撮合/风控)仍看真实账户,代理只作用于策略决策层。
  • 只在 live 注入(get_active() 非空);回测/影子不变。
  • 回归测试:代理属性映射表逐项测;不覆盖属性透传测试。

B3. 预算硬限制 + 默认值(用户定稿)

  • 预算载体 = live_accounts.initial_capital(含义升级为资金额度,不新增字段)。
  • 新建/修改校验(后端,fail-closed
    • 剩余 = snapshot.cash Σ 其他实盘实例预算(同共享池;绑专属账号的实例未来不参与)
    • 本次预算 > 剩余 → 400,报文带剩余数
    • 快照缺失/过期(>10min)→ 400 "账户快照不可用,稍后再试"(不猜数)
  • 前端:创建/编辑表单初始资金默认值 = 剩余(新接口 GET /live/budget-info{account_cash, allocated, remaining});实时占用率条(allocated/cash);超限红条+禁提交。
  • 存量实例不追溯;总览页持续显示占用率,存量超限亮黄条。
  • 影子/模拟盘不受此限制(虚拟资金)。

B4. 前端三层展示

实盘详情页三块(Tab 或分区):

  1. 本策略买卖live_trades 按 account(归因后只有本实例的成交)
  2. 本策略持仓+净值live_positions/live_balance(实例视图,已上线)
  3. 账户实况qmt_account_snapshot(现金/总资产/持仓列表 + Σ实例市值 + 未归因 的分解)

B5. 对账改造

15:10 日终对账改为恒等式优先:

  • 恒等式snapshot.market_value ≈ Σ 实例账本市值 + 未归因遗留仓(容差 0.5%,价格时点差)
  • 恒等式通过后,逐对 live↔shadow 行为对比(配对 v2 不变,instance_id 精确配对)
  • 未归因仓单列在报表里(重建后应为 0 或接近 0——这是"从新建开始"的直接红利)

B6. 收尾清理

  • 删光重建后:DELETE FROM live_balance/live_positions/live_trades WHERE account_id NOT IN (SELECT id FROM live_accounts)(孤儿行清理,一次性)
  • QMT remark bt:live_strateg:<hash> ↔ 实例映射查清(对账交叉验证用,顺手项)

C. 全删重建 Runbook(用户已定:一切从新建开始)

步骤 动作 时机
1 前端删除全部实盘实例 + 全部影子实例(删除即自动停引擎,配对从新) 用户(界面操作) 随时(今晚即可)
2 QMT 客户端手动卖出所有持仓释放资金 用户 明天开盘
3 B1→B5 开发、测试、pushCI→NAS 自动) 前后端 与 1/2 并行
4 策略 session 完成 A1/A2 策略 session 与 3 并行
5 合并部署 VPS(按口令闸门,用户说「推vps」) 用户授权 3/4 完成后
6 重建舰队:每策略 1 实盘 + 1 影子(4+4);实盘预算按用户数字(表单默认=剩余,硬限制兜底);必须在步骤 5 之后——虚拟化没上线前建的新实例仍会按全账户定寸 用户(界面)+ 前后端盯 部署完当晚
7 观察日:验证 A1/A3 验收标准 + 首次干净对账 前后端 重建次日

⚠️ 步骤 6 的顺序是硬约束:明天卖完资金后不要先建实例——新实例在虚拟化上线前依然按全账户定寸,995 万现金池会再次被竞赛。重建等部署完成。


D. 明确不做(触发式远期)

净额合成引擎 / 策略输出目标 / 中央风控 / 执行层——等真有 2+ 个不同策略并行时再立项。绑专属账号(账号口子)随时可插:插入后该实例预算=专属账号真实资金、账本=真实账本,本 spec 机制对其自然退化。