"""ZT 新路径测试脚手架:真实 Runtime/OrderBook/DipWatch + 模拟客户端。""" import tempfile from pathlib import Path from types import SimpleNamespace as NS from unittest.mock import Mock from libs.grid_take_profit import GridTrailingTracker from libs.order import OrderBook from libs.runtime import Runtime from libs.watch import DipWatch from sdk import Assets, DealItem, PositionItem, Tick from strategy.zt.rounds import Round, RoundStore, start_round ACCOUNT = 'zt-test' def account_cfg(**overrides): cfg = NS(account_id=ACCOUNT, strategy='zt', host_key='test', grid_step_pct=1.0, zt_open_hands=1, zt_sell_ratio=0.5, zt_buy_fall_pct=1.0, zt_max_price=200.0, zt_t_band_pct=1.0, zt_max_hold_days=5, min_cash_ratio=0.1, excluded_codes=[], signal_allow=['dcm'], buy_value=10000.0) for key, value in overrides.items(): setattr(cfg, key, value) return cfg def global_cfg(**overrides): cfg = NS(qmt_base_url='http://unused', qmt_token='', api_host='http://unused', qmt_data_dir='.', signals={}) for key, value in overrides.items(): setattr(cfg, key, value) return cfg class Fixture: """一套隔离的账户快照、轮次存储与运行上下文。""" def __init__(self, **cfg_overrides): self.tmp = tempfile.TemporaryDirectory() self.path = Path(self.tmp.name) self.rounds_path = self.path / f'zt_{ACCOUNT}_rounds.json' self.store = RoundStore(self.rounds_path) self.account_cfg = account_cfg(**cfg_overrides) self.assets = Assets(total=100000.0, available=100000.0) self.positions = {} self.orders = [] self.deals = [] self.ticks = {} self.client = self._client() self.run = Runtime( client=self.client, global_cfg=global_cfg(), account_cfg=self.account_cfg, orders=OrderBook(cancel_timeout_sec=300), open_watch=DipWatch(expire_seconds=600, rebound_threshold=0.0), add_watch=DipWatch(expire_seconds=600, rebound_threshold=0.0), profit_tracker=GridTrailingTracker(self.account_cfg.grid_step_pct), ) def _client(self): client = Mock() client.deals.side_effect = lambda: self.deals client.portfolio.side_effect = lambda: NS( assets=self.assets, positions=self.positions, orders=self.orders) client.full_tick.side_effect = lambda codes: dict(self.ticks) return client def cleanup(self): self.tmp.cleanup() # ---- 便捷构造 ---- def hold(self, code='600000.SH', volume=1000, price=10.0, can_use=None): position = PositionItem(stock_code=code, volume=volume, open_price=price, can_use_volume=volume if can_use is None else can_use) self.positions[code] = position return position def quote(self, code='600000.SH', price=10.0): self.ticks[code] = Tick(last_price=price) return self.ticks[code] def deal(self, local_id, volume, price, sys_id=None, code='600000.SH'): return DealItem(stock_code=code, order_sys_id=sys_id or f'{local_id}-{volume}', remark=local_id, offset_flag=48, volume=volume, price=price, trade_amount=price * volume, trade_date='20260915', trade_time='100000') def prime(self, watch, code, price): """让 DipWatch 先建立观察点,下一次同价或更高价即满足反弹确认。""" watch.triggered('prime', code, price) def tick(self, signals=()): """跑一轮 RunOnce,绕过真实时钟的交易时段判断。""" from unittest.mock import patch from strategy.zt import boot with patch.object(boot, 'trading_time', return_value=True): boot.RunOnce(self.run, self.store, list(signals)) return self.store def own_base(self, code='600000.SH', qty=1000, cost=10.0, today='2026-09-15'): """把该证券标记为"本策略自己建仓"(模拟建仓腿已成交)。""" item = Round(code=code, base_qty=qty, base_cost=cost, base_date=today, base_source='opened', phase='CLOSED', outcome='base') self.store.put(item) self.store.save() return item def open_round(self, code='600000.SH', kind='LONG_T', order_id='zt-entry-1', today='2026-09-15', **fields): """写入一条已提交开仓腿的轮次记录。""" item = Round(code=code, base_qty=fields.pop('base_qty', 1000), base_cost=fields.pop('base_cost', 10.0), base_source=fields.pop('base_source', 'opened')) start_round(item, kind, today) item.entry_order_id = order_id for key, value in fields.items(): setattr(item, key, value) self.store.put(item) self.store.save() return item @property def placed(self): return [call.kwargs for call in self.client.passorder.call_args_list]