From 1d4bdb206e0bc04ef1ea0f001e73e5d1f021359d Mon Sep 17 00:00:00 2001 From: claude_dev Date: Wed, 19 Aug 2026 21:53:36 +0800 Subject: [PATCH] =?UTF-8?q?docs(design):=20=E5=A4=9A=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E5=85=B1=E4=BA=AB=E8=B4=A6=E6=88=B7=C3=97=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E9=A2=84=E7=AE=97=E5=88=B6=E5=AE=9A=E7=A8=BFspec=E2=80=94?= =?UTF-8?q?=E2=80=94A=E7=AD=96=E7=95=A5session=E5=A5=91=E7=BA=A6(=E5=8D=96?= =?UTF-8?q?=E5=87=BA=E5=8F=AA=E5=8D=96=E8=87=AA=E5=B7=B1+context=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E6=B8=85=E5=8D=95+=E8=81=94=E8=B0=83)=20B=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E5=AE=9E=E6=96=BD(=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E7=9B=91=E8=A7=86=E5=99=A8/=E5=85=A8=E5=B1=80=E5=BF=AB?= =?UTF-8?q?=E7=85=A7/=E5=AE=9A=E5=AF=B8=E8=99=9A=E6=8B=9F=E5=8C=96/?= =?UTF-8?q?=E9=A2=84=E7=AE=97=E7=A1=AC=E9=99=90=E5=88=B6+=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=89=A9=E4=BD=99=E5=80=BC/=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E4=B8=89=E5=B1=82/=E5=AF=B9=E8=B4=A6=E6=81=92=E7=AD=89?= =?UTF-8?q?=E5=BC=8F/=E6=B8=85=E7=90=86)=20C=E5=85=A8=E5=88=A0=E9=87=8D?= =?UTF-8?q?=E5=BB=BArunbook(=E7=94=A8=E6=88=B7=E5=AE=9A:=E5=88=A0=E5=85=A8?= =?UTF-8?q?=E9=83=A8+=E6=98=8E=E5=BC=80=E7=9B=98=E6=B8=85=E4=BB=93+?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E5=90=8E=E5=86=8D=E9=87=8D=E5=BB=BA,?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E7=A1=AC=E7=BA=A6=E6=9D=9F)=20D=E8=BF=9C?= =?UTF-8?q?=E6=9C=9F=E8=A7=A6=E5=8F=91=E5=BC=8F;=E4=B8=9A=E7=95=8C?= =?UTF-8?q?=E8=B0=83=E7=A0=94=E8=83=8C=E4=B9=A6=E8=A7=81memory=20multi-str?= =?UTF-8?q?ategy-shared-account-plan=20[nas]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../multi-strategy-instance-budget-spec.md | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 docs/design/multi-strategy-instance-budget-spec.md diff --git a/docs/design/multi-strategy-instance-budget-spec.md b/docs/design/multi-strategy-instance-budget-spec.md new file mode 100644 index 0000000..798f7e8 --- /dev/null +++ b/docs/design/multi-strategy-instance-budget-spec.md @@ -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_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 账号一行): + +```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:` ↔ 实例映射查清(对账交叉验证用,顺手项) + +--- + +## 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 机制对其自然退化。