7eec983164
- 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当日无交叉, 撮合/存已单测)
185 lines
8.1 KiB
Python
185 lines
8.1 KiB
Python
"""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 增量推 NAS(run_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:
|
||
"""实走单日 step(scheduler 每日调)。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. 恢复 account(cash + 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 单)
|
||
# 实走每日单根 bar,ArrayManager 需 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 bar(Mac 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 close(fetch_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 完成 @%s,pending=%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_id(live_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_step(scheduler 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)
|