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

149 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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**
```python
# 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 的地基)
- supervisor`sanguo_live`)起一个账户监视线程:独立 probe 连接(session id 专用的 int),60s 查一次 QMT 账户资金+持仓,**upsert 一条**全局快照(不再挂任何实例名下)。
- 存储:新表 `qmt_account_snapshot`(按 QMT 账号一行):
```sql
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 机制对其自然退化。