docs(design): 多策略共享账户×实例预算制定稿spec——A策略session契约(卖出只卖自己+context属性清单+联调) B前后端实施(账户监视器/全局快照/定寸虚拟化/预算硬限制+默认剩余值/前端三层/对账恒等式/清理) C全删重建runbook(用户定:删全部+明开盘清仓+部署后再重建,顺序硬约束) D远期触发式;业界调研背书见memory multi-strategy-shared-account-plan [nas]
CI/CD / test (push) Successful in 17s
CI/CD / nas-deploy (push) Successful in 44s
CI/CD / nas-verify (push) Successful in 18s

This commit is contained in:
2026-08-19 21:53:36 +08:00
parent dae56e29aa
commit 1d4bdb206e
@@ -0,0 +1,148 @@
# 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 机制对其自然退化。