3aca14f723
run()循环体抽为step(bar_date,bars,prev_close,pending)→(pending,closes); run()改为调step。实走scheduler每日喂当日bar调step单步推进。 - 回放行为不变(test_engine原3用例pass) - 加test_engine_step_single_bar_advances: day1信号缓冲/day2撮合 - trader全量111/111通过
114 lines
4.7 KiB
Python
114 lines
4.7 KiB
Python
"""PaperEngine 主循环测试(mock data_source + mock 策略,spec §4)。"""
|
||
from types import SimpleNamespace
|
||
|
||
from sanguo_trader.account import Account
|
||
from sanguo_trader.cta_adapter import PaperCtaEngine
|
||
from sanguo_trader.engine import PaperEngine
|
||
from sanguo_trader.models import AccountConfig, MatchSession
|
||
from sanguo_trader.persistence import (
|
||
init_db, list_daily_balance, list_trades, save_account,
|
||
)
|
||
from sanguo_trader.strategy_runner import StrategyRunner
|
||
|
||
|
||
def _bar(date, o, h, l, c):
|
||
return SimpleNamespace(open_price=o, high_price=h, low_price=l,
|
||
close_price=c, datetime=date)
|
||
|
||
|
||
class _FakeDataSource:
|
||
def __init__(self, sections):
|
||
self.sections = sections
|
||
|
||
def iter_bars(self, symbols, start, end, interval, adjust="qfq", cfg=None):
|
||
del symbols, start, end, interval, adjust, cfg
|
||
for date, bars in self.sections:
|
||
yield date, bars
|
||
|
||
|
||
class _AlwaysBuyStrategy:
|
||
def __init__(self, engine, vt_symbol):
|
||
self.cta_engine = engine
|
||
self.vt_symbol = vt_symbol
|
||
|
||
def on_bar(self, bar):
|
||
self.cta_engine.send_order(self, "LONG", "OPEN", bar.close_price, 100)
|
||
|
||
|
||
def _build(tmp_path, sections, match_session=MatchSession.NEXT_OPEN):
|
||
db = str(tmp_path / "e.db")
|
||
init_db(db)
|
||
aid = save_account(db, {"name": "t", "initial_capital": 1_000_000})
|
||
cfg = AccountConfig(initial_capital=1_000_000)
|
||
account = Account(1_000_000)
|
||
cta = PaperCtaEngine("s1", match_session=match_session)
|
||
strat = _AlwaysBuyStrategy(cta, "600000.SSE")
|
||
cta.set_strategy(strat)
|
||
runner = StrategyRunner("s1", strategy=strat, paper_cta_engine=cta, symbol="600000")
|
||
pe = PaperEngine(account, [runner], _FakeDataSource(sections), cfg, db, aid,
|
||
symbols=["600000"], start="2024-01-01", end="2024-12-31")
|
||
return pe, db, aid, account, runner
|
||
|
||
|
||
def test_engine_next_open_fills_at_next_bar_open(tmp_path):
|
||
sections = [
|
||
("2024-01-01", {"600000": _bar("2024-01-01", 10.0, 10.5, 9.5, 10.0)}),
|
||
("2024-01-02", {"600000": _bar("2024-01-02", 10.5, 11.0, 10.0, 10.8)}),
|
||
("2024-01-03", {"600000": _bar("2024-01-03", 10.8, 11.5, 10.5, 11.0)}),
|
||
]
|
||
pe, db, aid, *_ = _build(tmp_path, sections)
|
||
pe.run()
|
||
fills = [t for t in list_trades(db, aid) if not t["rejected"]]
|
||
# day1 信号→day2 撮合@10.5(open);day2 信号→day3 撮合@10.8(open);day3 信号无day4
|
||
assert len(fills) == 2
|
||
assert fills[0]["price"] == 10.5
|
||
assert fills[1]["price"] == 10.8
|
||
|
||
|
||
def test_engine_current_close_fills_same_bar(tmp_path):
|
||
sections = [
|
||
("2024-01-01", {"600000": _bar("2024-01-01", 10.0, 10.5, 9.5, 10.0)}),
|
||
("2024-01-02", {"600000": _bar("2024-01-02", 10.5, 11.0, 10.0, 10.8)}),
|
||
]
|
||
pe, db, aid, *_ = _build(tmp_path, sections, match_session=MatchSession.CURRENT_CLOSE)
|
||
pe.run()
|
||
fills = [t for t in list_trades(db, aid) if not t["rejected"]]
|
||
assert len(fills) == 2
|
||
assert fills[0]["price"] == 10.0 # 当根 close
|
||
assert fills[1]["price"] == 10.8
|
||
|
||
|
||
def test_engine_daily_balance_and_consistency(tmp_path):
|
||
sections = [
|
||
("2024-01-01", {"600000": _bar("2024-01-01", 10.0, 10.5, 9.5, 10.0)}),
|
||
("2024-01-02", {"600000": _bar("2024-01-02", 10.5, 11.0, 10.0, 10.8)}),
|
||
("2024-01-03", {"600000": _bar("2024-01-03", 10.8, 11.5, 10.5, 11.0)}),
|
||
]
|
||
pe, db, aid, account, runner = _build(tmp_path, sections)
|
||
pe.run()
|
||
balances = list_daily_balance(db, aid)
|
||
assert len(balances) == 3
|
||
# 总账持仓 = 分户持仓(day2+day3 各买100 = 200)
|
||
assert account.positions["600000"].volume == 200
|
||
assert runner.positions["600000"].volume == 200
|
||
|
||
|
||
def test_engine_step_single_bar_advances(tmp_path):
|
||
"""engine.step 单根推进(C-S3 实走每日入口):day1 信号缓冲,day2 撮合。"""
|
||
sections = [
|
||
("2024-01-01", {"600000": _bar("2024-01-01", 10.0, 10.5, 9.5, 10.0)}),
|
||
("2024-01-02", {"600000": _bar("2024-01-02", 10.5, 11.0, 10.0, 10.8)}),
|
||
]
|
||
pe, db, aid, account, runner = _build(tmp_path, sections)
|
||
# day1 step:_AlwaysBuyStrategy 买单(NEXT_OPEN)→ 进 pending,当根不撮合
|
||
pending, closes = pe.step("2024-01-01", sections[0][1], {}, [])
|
||
assert len(pending) == 1
|
||
assert account.positions.get("600000") is None
|
||
assert closes["600000"] == 10.0
|
||
# day2 step:撮合 day1 pending @ open 10.5;策略 on_bar(day2) 又发单进 pending 等 day3
|
||
pending2, closes2 = pe.step("2024-01-02", sections[1][1], closes, pending)
|
||
assert len(pending2) == 1 # day2 新信号(无 day3 不撮合)
|
||
assert account.positions["600000"].volume == 100 # day1 单 day2 open 10.5 撮合 100 股
|
||
# step 入库(day1+day2 各一条余额)
|
||
assert len(list_daily_balance(db, aid)) == 2
|