Files
sanguo_vnpy_v2/sanguo_trader/engine.py
T
claude_dev 42877213ae feat(trader): PaperEngine 主循环(逐bar重放/next_open缓冲/current_close/双层记账/持久化)
- run(): T+1解冻→撮合上根pending(用当前bar)→喂策略收单→current_close当根/next_open缓冲→盯市入库
- 双层记账一致性(总账=分户之和), checkpoint续跑字段
- StrategyRunner +symbol 字段 3 tests passed.
2026-07-07 12:01:37 +08:00

113 lines
4.7 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.
"""PaperEngine 模拟盘主循环(逐根 bar 重放 + 双层记账 + 持久化,spec §4/§9)。
run():逐 bar → T+1 解冻 → 撮合上一根 next_open pending(用当前 bar)→ 喂策略
on_bar 收新单 → current_close 当根撮合 / next_open 缓冲到下根 → 盯市 → 入库。
raw 首版 fallback qfqspec §17),信号与撮合共用一套(标注)。
"""
import logging
import pandas as pd
from .account import Account
from .matcher import cross_order
from .models import MatchSession, PaperTrade
from .persistence import save_daily_balance, save_trade, update_checkpoint
from .strategy_runner import StrategyRunner
logger = logging.getLogger(__name__)
def _to_series(bar) -> pd.Series:
return pd.Series({
"open": bar.open_price, "high": bar.high_price,
"low": bar.low_price, "close": bar.close_price,
"date": getattr(bar, "datetime", ""),
})
def _trade_to_dict(t: PaperTrade, bar_date) -> dict:
return {
"strategy_id": t.strategy_id, "symbol": t.symbol,
"direction": t.side.value, "price": t.price, "volume": t.volume,
"commission": t.commission, "stamp_duty": t.stamp_duty,
"transfer_fee": t.transfer_fee,
"bar_date": str(bar_date), "match_session": t.match_session.value,
}
class PaperEngine:
def __init__(self, account: Account, runners: list[StrategyRunner],
data_source, cfg, db_path: str, account_id: int,
symbols: list[str], start: str, end: str,
interval: str = "d") -> None:
self.account = account
self.runners = runners
self.data_source = data_source
self.cfg = cfg
self.db_path = db_path
self.account_id = account_id
self.symbols = symbols
self.start = start
self.end = end
self.interval = interval
def run(self) -> None:
prev_close: dict[str, float] = {}
pending: list = [] # [(order, runner)] next_open 待下根撮合
bar_count = 0
for bar_date, bars in self.data_source.iter_bars(
self.symbols, self.start, self.end, self.interval, "qfq", None
):
bar_count += 1
self.account.unfreeze_all()
for r in self.runners:
r.unfreeze_all()
# 1. 撮合上一根 pendingnext_open,用当前 bar
if pending:
for order, runner in pending:
self._match(order, runner, bars, prev_close, bar_date)
pending = []
# 2. 喂策略 on_bar → 收新单
for runner in self.runners:
sym = runner.symbol
if sym and sym in bars:
runner.paper_cta_engine.on_bar(bars[sym])
for order in runner.paper_cta_engine.pop_orders():
if order.match_session == MatchSession.NEXT_OPEN:
pending.append((order, runner))
else: # current_close 当根撮合
self._match(order, runner, bars, prev_close, bar_date)
# 3. 盯市 + 入库
closes = {s: bars[s].close_price for s in bars}
self.account.mark_to_market(closes)
save_daily_balance(
self.db_path, self.account_id, str(bar_date),
self.account.cash, self.account.market_value, self.account.equity,
is_checkpoint=(bar_count % 500 == 0),
)
update_checkpoint(self.db_path, self.account_id, str(bar_date))
prev_close = closes
def _match(self, order, runner, bars, prev_close, bar_date) -> None:
if order.symbol not in bars:
return
match_bar = _to_series(bars[order.symbol])
pc = prev_close.get(order.symbol, order.price)
result = cross_order(order, match_bar, pc, self.cfg)
if isinstance(result, PaperTrade):
if self.account.cash_enough(result):
self.account.apply_trade(result)
runner.apply_trade(result)
save_trade(self.db_path, self.account_id,
_trade_to_dict(result, bar_date))
else:
save_trade(self.db_path, self.account_id,
{"strategy_id": order.strategy_id, "symbol": order.symbol,
"bar_date": str(bar_date)},
rejected=True, reject_reason="insufficient_cash")
else: # PaperReject
save_trade(self.db_path, self.account_id,
{"strategy_id": order.strategy_id, "symbol": order.symbol,
"bar_date": str(bar_date)},
rejected=True, reject_reason=result.reason)