8.9 KiB
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_test(rotate/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 的地基)
- supervisor(
sanguo_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= 实例账本 cashtotal_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 或分区):
- 本策略买卖:
live_trades按 account(归因后只有本实例的成交) - 本策略持仓+净值:
live_positions/live_balance(实例视图,已上线) - 账户实况:
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 开发、测试、push(CI→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 机制对其自然退化。