"""保存交易意图与网格基准;实际持仓始终以券商快照为准。""" from dataclasses import asdict, dataclass, field from pathlib import Path import json import math from libs.lockfile import replace_json @dataclass class SymbolState: volume: int = 0 cost: float = 0.0 last_buy: float = 0.0 armed: bool = False sell_grid: float = 0.0 peak: int = 0 pending: dict = field(default_factory=dict) def reset_profit(self): """持仓成本或数量改变后,不沿用上轮止盈峰值。""" self.armed = False self.sell_grid = 0.0 self.peak = 0 class Store: def __init__(self, path: Path, account: str): self.path, self.account = path, account self.symbols: dict[str, SymbolState] = {} if path.exists(): try: raw = json.loads(path.read_text(encoding='utf-8')) if raw['version'] != 1 or raw['account'] != account: raise ValueError('版本或账户不一致') for code, value in raw['symbols'].items(): state = SymbolState(**value) if type(state.volume) is not int or state.volume < 0 or type(state.peak) is not int: raise ValueError('状态数量或峰值无效') if any(not math.isfinite(v) or v < 0 for v in (state.cost, state.last_buy, state.sell_grid)): raise ValueError('状态价格无效') if type(state.armed) is not bool or (state.armed and state.sell_grid <= 0): raise ValueError('止盈状态无效') if not isinstance(state.pending, dict): raise ValueError('委托状态无效') if state.pending: p = state.pending if (p['side'] not in ('BUY', 'SELL') or not p['id'].startswith('ETF-') or type(p['volume']) is not int or p['volume'] <= 0 or (p['side'] == 'BUY' and p['volume'] > 1000) or type(p['base_volume']) is not int or p['base_volume'] < 0 or not math.isfinite(p['reserved']) or p['reserved'] < 0): raise ValueError('待确认委托无效') self.symbols[code] = state except (ValueError, KeyError, TypeError, AttributeError) as exc: raise ValueError(f'ETF 状态损坏,禁止自动重建:{path}') from exc def get(self, code: str) -> SymbolState: return self.symbols.setdefault(code, SymbolState()) def save(self): self.path.parent.mkdir(parents=True, exist_ok=True) replace_json(self.path, dict(version=1, account=self.account, symbols={k: asdict(v) for k, v in self.symbols.items()}))