"""做 T 策略的持仓状态和逐笔实际成交记录。""" from __future__ import annotations import math from dataclasses import dataclass from datetime import datetime from pathlib import Path from sdk import DealItem, PositionItem from libs.orderbook import OrderBook READY, SOLD, DONE = "READY", "SOLD", "DONE" @dataclass(slots=True) class TStateItem: code: str base_qty: int = 0 base_cost: float = 0.0 trade_date: str = "" phase: str = READY sell_qty: int = 0 sell_price: float = 0.0 buy_qty: int = 0 buy_cost: float = 0.0 id: int = 0 base_order_id: str = '' added_order_id: str = '' added_num: int = 0 added_qty: int = 0 added_cost: float = 0.0 class TState: """Apply actual executions immediately, atomically with their position changes.""" def __init__(self, path: str | Path) -> None: self._store = OrderBook(path) self.path = self._store.path self._load() @staticmethod def _is_zt_deal(deal: DealItem) -> bool: return ( deal.side == 'BUY' and deal.local_order_id.startswith(('zt-base-', 'zt-t-buy-')) ) or ( deal.side == 'SELL' and deal.local_order_id.startswith('zt-t-sell-') ) @staticmethod def _reset(item: TStateItem, date: str) -> bool: if item.phase == DONE and item.trade_date != date: item.phase, item.trade_date = READY, '' item.sell_qty = item.buy_qty = 0 item.sell_price = item.buy_cost = 0.0 return True return False @classmethod def _apply_t_deal(cls, item: TStateItem, deal: dict) -> None: """实时入账与重启恢复共用同一套做 T 轮次计算。""" cls._reset(item, deal['insert_date']) qty, amount = deal['traded_volume'], deal['trade_amount'] if deal['side'] == 'SELL': total = item.sell_qty + qty item.sell_price = (item.sell_qty * item.sell_price + amount) / total item.sell_qty = total item.phase = SOLD else: total = item.buy_qty + qty item.buy_cost = (item.buy_qty * item.buy_cost + amount) / total item.buy_qty = total item.phase = DONE if total >= item.sell_qty else SOLD item.trade_date = deal['insert_date'] def reconcile( self, positions: list[PositionItem], deals: list[DealItem] ) -> None: """Deduplicate each fill; partial fills do not wait for order completion.""" today = datetime.now().date().isoformat() seen = {row['sys_order_id'] for row in self.deals} rows = [] for deal in deals: if not self._is_zt_deal(deal) or deal.sys_order_id in seen: continue try: row = self._store.deal_record(deal) except ValueError: continue rows.append(row) seen.add(deal.sys_order_id) rows.sort(key=lambda r: (r['insert_date'], r['insert_time'])) modified = False try: # Snapshot includes these fills: subtract their net quantity before replay. net = {} for row in rows: net[row['code']] = net.get(row['code'], 0) + ( row['traded_volume'] if row['side'] == 'BUY' else -row['traded_volume'] ) for position in positions: code = position.stock_code if code in self.items or position.volume <= 0: continue if not math.isfinite(position.open_price) or position.open_price <= 0: continue qty = max(0, position.volume - net.get(code, 0)) self.items[code] = TStateItem(code, qty, position.open_price if qty else 0.0) modified = True # 全部卖出时快照可能已无该证券,按净卖出数量恢复待买回的底仓数量。 for code, delta in net.items(): if code not in self.items and delta < 0: self.items[code] = TStateItem(code, -delta) for row in rows: item = self.items.setdefault(row['code'], TStateItem(row['code'])) self._reset(item, row['insert_date']) qty, amount = row['traded_volume'], row['trade_amount'] if row['local_order_id'].startswith('zt-base-'): total = item.base_qty + qty item.base_cost = (item.base_qty * item.base_cost + amount) / total item.base_qty = total item.base_order_id = row['local_order_id'] else: self._apply_t_deal(item, row) self.deals.append(row) modified = True for item in self.items.values(): modified = self._reset(item, today) or modified if modified: self.save() except Exception: self._load() raise def save(self) -> None: try: self._store.save( { code: { 'code': item.code, 'base_order_id': item.base_order_id, 'base_qty': item.base_qty, 'base_cost': item.base_cost, 'added_order_id': item.added_order_id, 'added_num': item.added_num, 'added_qty': item.added_qty, 'added_cost': item.added_cost, 'status': item.phase, } for code, item in self.items.items() }, self.deals, ) except Exception: self._load() raise def _load(self) -> None: self._store.load() self.items = {} for code, position in self._store.positions.items(): position = dict(position) position['phase'] = position.pop('status') self.items[code] = TStateItem(**position) self.deals = [ {key: value for key, value in deal.items() if key != 'id'} for deal in self._store.deals.values() ] # 轮次明细不占用持仓表字段,从已保存的逐笔成交重建。 for deal in self.deals: if not deal['local_order_id'].startswith('zt-base-') and deal['code'] in self.items: self._apply_t_deal(self.items[deal['code']], deal) for code, item in self.items.items(): if self._store.positions[code]['status'] == READY: item.phase, item.trade_date = READY, '' item.sell_qty = item.buy_qty = 0 item.sell_price = item.buy_cost = 0.0