diff --git a/sanguo_portfolio/runner_live.py b/sanguo_portfolio/runner_live.py index fda5711..07e499d 100644 --- a/sanguo_portfolio/runner_live.py +++ b/sanguo_portfolio/runner_live.py @@ -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", "")), diff --git a/tests/portfolio/test_live_instance_ledger.py b/tests/portfolio/test_live_instance_ledger.py index 95a4e99..b2c13b9 100644 --- a/tests/portfolio/test_live_instance_ledger.py +++ b/tests/portfolio/test_live_instance_ledger.py @@ -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