fix(live): instance虚拟账本+成交归因+实盘日志黑洞——共享QMT账户三害根治第一步 [vps]
CI/CD / test (push) Successful in 11s
CI/CD / nas-deploy (push) Successful in 34s
CI/CD / nas-verify (push) Successful in 11s

2026-08-19盘后QMT实锤:①互卖当日真实发生(518880两个实例各卖183700、
600036两实例各卖42900)②买入sizing被全账户污染(channel_test实盘买
183700股@9.12≈168万=全账户995万/6,影子才16.8万=100万/6)③QMT委托带
remark=bt:live_strateg:<hash>实例指纹(归因可交叉验证)。

①live_instance_ledger(新模块):每live实例一份虚拟子账本(现金=初始−Σ买
−Σ费+Σ卖,持仓+移动加权成本+T+1当日买入锁定,线程安全锁),由**本实例
真实成交**驱动——engine.get_trades()按order_id∈engine.get_orders()归因
(引擎_broker_order_index已映射回本实例id空间),别家实例/手动单不进账;
卖超账面/无账面卖出如实留痕不崩;restore_from_trades重启恢复。
②runner_live:_sync_trades旧轮询(写不进live_trades的坏件)替换为归因
落库(方向取自订单is_buy);快照落库从context.portfolio全账户改**实例视图**
(positions=账本持仓T+1冻结;balance=虚拟现金+市值,现价取全账户快照/退
加权成本)——治8实例同写一份全账户持仓+收益率=全账户/初始资金无意义;
balance节流:有成交立即写否则≥5分钟(治1440行/天/实例量偏大遗留)。
③通道注入:BrokerFacade.get_instance_positions字段+live_strategy._setup
读get_active()注入positions_view(策略session消费,getattr兜底回退
context.portfolio;回测/单测无账本=保持None)。
④实盘引擎日志黑洞根治:supervisor子进程stdout/stderr落logs/live_{aid}.log
(>5MB截断+spawn标记,对齐影子#88同款修法;8-17起实盘委托/成交零留存)。
+15测试(算术/归因过滤/幂等/快照实例视图/通道注入/日志重定向);
portfolio+live+shadow 400绿。
This commit is contained in:
2026-08-19 18:39:13 +08:00
parent 78d35baae5
commit dae56e29aa
7 changed files with 629 additions and 92 deletions
+51 -9
View File
@@ -278,32 +278,37 @@ def test_update_live_normalizes_vt_symbol(tmp_path, monkeypatch):
assert acc["vt_symbol"] == "300024.SZSE"
def test_snapshot_once_skips_unsynced_cash():
"""cash<=0(账户未同步完成)不落 balance——治假收益率(2026-08-14 实况 341080%)。"""
def test_snapshot_once_broker_snapshot_never_pollutes_balance():
"""balance=实例虚拟账本(2026-08-19):broker 快照 cash=0/乱值不落库——
旧版治假收益率(341080%)的守卫换形态:不再读 broker 现金,快照只供现价。"""
import os
import tempfile
import types
from sanguo_live.persistence import init_db, list_balance
from sanguo_portfolio.live_instance_ledger import LiveInstanceLedger
from sanguo_portfolio.runner_live import _snapshot_once
db = os.path.join(tempfile.mkdtemp(), "l.db")
init_db(db)
led = LiveInstanceLedger(initial_cash=1_000_000)
led.apply_trade(True, "000001.XSHE", 10.0, 100, "t1", "2026-08-19")
def _mk_portfolio(cash, total):
p = types.SimpleNamespace(
available_cash=cash, total_value=total, positions={})
available_cash=cash, total_value=total,
positions={"000001.XSHE": types.SimpleNamespace(price=0.0)})
ctx = types.SimpleNamespace(portfolio=p)
return types.SimpleNamespace(context=ctx)
# 未同步完成: cash=0, total=持仓市值 → 不落 balance
_snapshot_once(_mk_portfolio(0, 2931.0), db, 3)
assert list_balance(db, 3) == []
# 正常: cash>0 → 落库
_snapshot_once(_mk_portfolio(9_997_077.51, 10_000_008.51), db, 3)
# broker 未同步(cash=0/无现价):balance 仍=账本算术(现金+加权成本市值)
_snapshot_once(_mk_portfolio(0, 2931.0), db, 3, led)
rows = list_balance(db, 3)
assert len(rows) == 1
assert rows[0]["total"] == 10_000_008.51
assert rows[0]["cash"] == 999_000.0 - 5.0
assert rows[0]["market_value"] == 100 * 10.0 # 现价缺失退加权成本
# broker 快照的现金/总值完全不被引用(共享全账户数字不进实例表)
assert rows[0]["total"] == rows[0]["cash"] + rows[0]["market_value"]
def test_get_live_return_uses_first_snapshot_baseline(live_db):
@@ -317,3 +322,40 @@ def test_get_live_return_uses_first_snapshot_baseline(live_db):
acc = rl.get_live(aid)
assert acc["latest_equity"] == 10_150_000
assert acc["total_return"] == (10_150_000 - 10_000_000) / 10_000_000
def test_start_portfolio_subprocess_redirects_child_output(tmp_path, monkeypatch):
"""实盘引擎子进程 stdout/stderr 落 logs/live_{aid}.log(2026-08-19 黑洞根治)。
8-17 起实盘引擎继承 schtask 控制台(=黑洞),委托/成交/异常零留存——影子侧
2026-08-16 已修同款,这里对齐:spawn 标记 + >5MB 截断 + Popen 收 stdout。
"""
# __file__ 指到 tmp,日志目录落在 tmp/logs 不污染仓库
fake_file = tmp_path / "sanguo_live" / "runner.py"
fake_file.parent.mkdir(parents=True)
fake_file.write_text("# probe", encoding="utf-8")
monkeypatch.setattr(live_runner, "__file__", str(fake_file))
captured = {}
def fake_popen(argv, env=None, stdout=None, stderr=None):
captured["argv"] = argv
captured["stderr"] = stderr
stdout.write("probe-line\n".encode())
stdout.flush()
stdout.close()
return object()
monkeypatch.setattr(live_runner.subprocess, "Popen", fake_popen)
live_runner._start_portfolio_subprocess(
{"id": 77, "account": "66639661", "strategy_class": "channel_test",
"setting": "{}"}, str(tmp_path / "l.db"))
assert captured["argv"][1:] == ["-X", "utf8", "-m",
"sanguo_portfolio.runner_live"]
assert captured["stderr"] is not None # stderr 并入 stdout
log = tmp_path / "logs" / "live_77.log"
assert log.exists()
text = log.read_text(encoding="utf-8")
assert "==== spawn" in text
assert "probe-line" in text