Files
sanguo_vnpy_v2/sanguo_trader/live_orchestrator.py
T
claude_dev 7eec983164 fix(live): C-S3实走warmup(am跨日)+fetch_day wrapper+端到端验证
- live_orchestrator warmup: 重放start~昨日raw到策略am使其inited(实走每日单根, 不warmup则ArrayManager永不inited→策略无信号)
- routes _DataSourceWrapper 加 fetch_day(给 live_step 拉当日raw)
- verify_live_step 容器端到端: 创建live account+live_step(07-07 warmup+step)+存pending, 跑通(pending=0系DoubleMa当日无交叉, 撮合/存已单测)
2026-07-08 06:59:24 +08:00

185 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""C-S3 实走编排(task 2):每日 live_step 单根推进。
架构(简化,避开 APScheduler per-account 闭包注入):
- 容器 APScheduler 每日 20:30 调 live_runner.run_live_step(全局 job,遍历 live accounts
- live_step(account_id) 自包含:恢复状态 → 读当日 raw bar → engine.step → 存状态
- 当日 raw 由 Mac launchd 增量推 NASrun_daily_update.sh 加 raw 增量)
状态恢复:cash=最后余额 / positions=paper_positions / pending=paper_pending_orders
TODO(分期项):prev_close 从昨日 raw close 读(首版用当日 open 兜底);
listing_days 从 IPO 日算(首版 stub 0);realized_pnl 未恢复(归因次日重置)。
"""
import json
import logging
import sqlite3
from datetime import datetime, timedelta
from .account import Account
from .cta_adapter import PaperCtaEngine
from .engine import PaperEngine
from .limit import lot_size_for
from .models import AccountConfig, MatchSession, OrderSide, PaperOrder
from .position_ledger import PositionLedger
from .persistence import (
load_last_balance, load_positions, save_positions,
load_pending_orders, save_pending_orders,
)
from .strategy_runner import StrategyRunner
logger = logging.getLogger(__name__)
def _get_account(db_path: str, account_id: int) -> dict:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
return dict(conn.execute(
"SELECT * FROM paper_accounts WHERE id=?", (account_id,)
).fetchone())
def _restore_ledger(positions: dict) -> dict:
"""{symbol:{volume,frozen,avg_price}} → {symbol: PositionLedger}。"""
return {sym: PositionLedger(sym, p["volume"], p.get("frozen", 0), p["avg_price"])
for sym, p in positions.items()}
def live_step(db_path: str, account_id: int, data_source, cfg, today: str | None = None) -> None:
"""实走单日 stepscheduler 每日调)。data_source=_DataSourceWrapper, cfg=data 配置。
today: 默认 datetime.now();验证可指定历史日期(有 raw)。
"""
from sanguo_data.datareader import guess_exchange
from sanguo_api.strategy_registry import get_strategy_class
from vnpy.trader.utility import ArrayManager
acc = _get_account(db_path, account_id)
symbols = json.loads(acc["symbols"] or "[]")
strategies = json.loads(acc["strategies"] or "[]")
interval = acc.get("interval") or "d"
initial = acc["initial_capital"]
# 1. 恢复 accountcash + positions
account = Account(initial)
last_bal = load_last_balance(db_path, account_id)
if last_bal:
account.cash = last_bal["cash"]
account.positions = _restore_ledger(load_positions(db_path, account_id, "account"))
# 2. 构造 runners(恢复 positions
runners: list = []
for s in strategies:
cls = get_strategy_class(s["name"])
if cls is None:
continue
cta = PaperCtaEngine(s["name"], match_session=s.get("match_session", "next_open"),
listing_days=s.get("listing_days", 0),
size=lot_size_for(s["symbol"]))
vt = f"{s['symbol']}.{guess_exchange(s['symbol']).value}"
strat = cls(cta, s["name"], vt, s.get("params", {}))
strat.trading = True
if not hasattr(strat, "am"):
strat.am = ArrayManager(20)
cta.set_strategy(strat)
runner = StrategyRunner(s["name"], strategy=strat, paper_cta_engine=cta,
symbol=s["symbol"])
runner.positions = _restore_ledger(
load_positions(db_path, account_id, f"strategy:{s['name']}"))
runners.append(runner)
if not runners:
logger.warning("live_step %s: 无可用策略(容器缺 vnpy_ctastrategy?),跳过", account_id)
return
# 3. 日期 + warmup(重放 start~昨日到策略 am 使其 inited;丢弃 warmup 单)
# 实走每日单根 barArrayManager 需 warmup 才 inited,否则策略无信号。
today = today or datetime.now().strftime("%Y-%m-%d")
yesterday = (datetime.strptime(today, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d")
start_date = acc.get("start_date") or today
if start_date < yesterday:
for _wd, wbars in data_source.iter_bars(
symbols, start_date, yesterday, interval, adjust="raw", cfg=cfg
):
for runner in runners:
if runner.symbol in wbars:
runner.paper_cta_engine.on_bar(wbars[runner.symbol])
runner.paper_cta_engine.pop_orders() # warmup 单丢弃(不撮合)
# 4. 当日 raw barMac launchd 已推 NAS
bars = {}
for sym in symbols:
bar = data_source.fetch_day(sym, today, interval, adjust="raw", cfg=cfg)
if bar is not None:
bars[sym] = bar
if not bars:
logger.info("live_step %s: 当日无 raw bar%s 非交易日或未推?),跳过", account_id, today)
return
# 5. 恢复 pending + prev_close
pending = []
for o in load_pending_orders(db_path, account_id):
runner = next((r for r in runners if r.strategy_id == o["strategy_id"]), None)
if runner is None:
continue
pending.append((PaperOrder(
o["strategy_id"], o["symbol"], OrderSide(o["side"]), o["price"], o["volume"],
o["is_market"], MatchSession(o["match_session"]), o["listing_days"],
), runner))
# prev_close:昨日 raw closefetch_day 昨日);失败兜底用当日 open
prev_close = {}
for sym in symbols:
ybar = data_source.fetch_day(sym, yesterday, interval, adjust="raw", cfg=cfg)
prev_close[sym] = ybar.close_price if ybar else bars[sym].open_price
# 5. engine.step(单根当日)
acc_cfg = AccountConfig(
initial_capital=initial, rate=acc["rate"], slippage=acc["slippage"],
pricetick=acc["pricetick"], stamp_duty_rate=acc["stamp_duty_rate"],
transfer_fee_rate=acc["transfer_fee_rate"], min_commission=acc["min_commission"],
)
pe = PaperEngine(account, runners, data_source, acc_cfg, db_path, account_id,
symbols, acc.get("start_date") or today, today, interval, adjust="raw")
pending_new, _closes = pe.step(today, bars, prev_close, pending)
# 6. 存状态(pending + positions
save_pending_orders(db_path, account_id, [
{"strategy_id": o.strategy_id, "symbol": o.symbol, "side": o.side.value,
"price": o.price, "volume": o.volume, "is_market": o.is_market,
"match_session": o.match_session.value, "listing_days": o.listing_days}
for o, _r in pending_new
])
save_positions(db_path, account_id, "account", {
sym: {"volume": p.volume, "frozen": p.frozen, "avg_price": p.avg_price}
for sym, p in account.positions.items()}, today)
for r in runners:
save_positions(db_path, account_id, f"strategy:{r.strategy_id}", {
sym: {"volume": p.volume, "frozen": p.frozen, "avg_price": p.avg_price}
for sym, p in r.positions.items()}, today)
logger.info("live_step %s 完成 @%spending=%d positions=%d",
account_id, today, len(pending_new), len(account.positions))
def list_live_accounts(db_path: str) -> list[int]:
"""所有 mode=live & status=running 的 account_idlive_runner 遍历用)。"""
with sqlite3.connect(db_path) as conn:
rows = conn.execute(
"SELECT id FROM paper_accounts WHERE mode='live' AND status='running'"
).fetchall()
return [r[0] for r in rows]
def run_live_step(db_path: str) -> None:
"""每日全局 step:遍历所有 live accounts 调 live_stepscheduler 20:30 调)。
lazy import _DataSourceWrapper 避免与 routes_paper 循环 import。
"""
from sanguo_data.config import find_config_path, load_config
from sanguo_api.routes_paper import _DataSourceWrapper
cfg = load_config(find_config_path())
data_source = _DataSourceWrapper(cfg)
for aid in list_live_accounts(db_path):
try:
live_step(db_path, aid, data_source, cfg)
except Exception as e: # noqa: BLE001
logger.error("live_step account %s 失败: %s", aid, e)