fix(live): 成交时间1970守卫——08-20探针首日实锤9笔traded_at=1970-01-01 00:00:01(QMT原始时间经引擎pd.to_datetime失败形态落epoch),前端『今日成交』按日期过滤全空+账本trade_date失真(当日仓被当历史仓,T+1视图frozen=0);修=_effective_trade_time守卫:年份<2000一律回退当前时刻(归因轮询≤60s,日期误差仅跨日60s窗口);+2测试(1970回退当日记账且frozen生效/有效时间原样保留);附带已治疗VPS存量9行(从引擎日志按标的+数量回填13:03/13:05真实时刻,#18今日成交即时可见);portfolio 29+398绿 [vps]

This commit is contained in:
2026-08-20 13:32:54 +08:00
parent 2191160dda
commit b436adef1b
2 changed files with 54 additions and 2 deletions
+16 -2
View File
@@ -60,6 +60,20 @@ def live_env() -> Dict[str, str]:
}
def _effective_trade_time(trade: Any) -> Any:
"""成交时间守卫(2026-08-20 事故):QMT 原始成交时间经引擎 pd.to_datetime 的
失败形态会落成 1970-01-01 00:00:01 的 datetime——当日 9 笔 traded_at=1970
落库,前端"今日成交"按日期过滤全空 + 账本 trade_date 失真(T+1 视图错)。
年份<2000 一律视为无效,回退当前时刻:归因轮询间隔 ≤60s,日期误差只剩
跨日 60s 窗口,可忽略。"""
from datetime import datetime
v = getattr(trade, "time", None)
if isinstance(v, datetime) and v.year >= 2000:
return v
return datetime.now()
def _sync_instance_trades(
engine: Any, ledger: Any, db: str, account_id: int, strategy_name: str,
) -> None:
@@ -88,8 +102,8 @@ def _sync_instance_trades(
if oid not in own_buy:
continue # 别家实例/手动单,不归因给本实例
is_buy = own_buy[oid]
t_time = getattr(t, "time", None)
date_str = t_time.strftime("%Y-%m-%d") if hasattr(t_time, "strftime") else str(t_time or "")
t_time = _effective_trade_time(t)
date_str = t_time.strftime("%Y-%m-%d")
applied = ledger.apply_trade(
is_buy=is_buy,
symbol=str(getattr(t, "security", "")),
@@ -129,6 +129,44 @@ def _fake_engine():
class TestAttributionAndSnapshot:
def test_sync_time_guard_rejects_epoch_1970(self, tmp_path):
"""2026-08-20 事故回归:引擎成交 time 落 1970 → traded_at/账本 trade_date
必须回退当前时刻(否则前端今日成交全空、T+1 视图把当日仓当历史仓)。"""
from datetime import date as _date
from sanguo_live.persistence import init_db, list_trades
db = str(tmp_path / "live.db")
init_db(db)
led = LiveInstanceLedger(initial_cash=1_000_000)
own = SimpleNamespace(order_id="o1", is_buy=True)
t_bad = SimpleNamespace(
order_id="o1", security="000001.XSHE", amount=100, price=10.0,
time=datetime(1970, 1, 1, 0, 0, 1), commission=0.0, tax=0.0)
engine = SimpleNamespace(
get_orders=lambda: {"o1": own},
get_trades=lambda: {"t1": t_bad},
context=SimpleNamespace(portfolio=SimpleNamespace(positions={})),
)
_sync_instance_trades(engine, led, db, 44, "channel_test")
rows = list_trades(db, 44)
assert len(rows) == 1
assert rows[0]["traded_at"].startswith(_date.today().isoformat())
# 账本按今天记账 → 当日买入冻结(T+1),不再被当历史仓
view = led.positions_view(_date.today().isoformat())
assert view["000001.XSHE"]["closeable_amount"] == 0
def test_sync_time_guard_keeps_valid_time(self, tmp_path):
from sanguo_live.persistence import init_db, list_trades
db = str(tmp_path / "live.db")
init_db(db)
led = LiveInstanceLedger(initial_cash=1_000_000)
engine = _fake_engine()
_sync_instance_trades(engine, led, db, 44, "channel_test")
rows = list_trades(db, 44)
assert rows[0]["traded_at"] == "2026-08-19 09:35:00" # 有效时间原样保留
def test_sync_attributes_only_own_orders(self, tmp_path):
from sanguo_live.persistence import init_db, list_trades