fix state.py

This commit is contained in:
2026-09-05 15:56:39 +08:00
parent ba1bd93afa
commit 1fc3e119d3
4 changed files with 78 additions and 119 deletions

View File

@@ -61,7 +61,7 @@ def StartTrend() -> None:
config.account_config.strategy, config.account_config.strategy,
config.account_config.account_id, config.account_config.account_id,
) )
order_book = OrderBook(state=storeState) order_book = OrderBook()
order_book.refresh(client, portfolio.orders) order_book.refresh(client, portfolio.orders)
storeState.reconcile(positions, portfolio.orders) storeState.reconcile(positions, portfolio.orders)

View File

@@ -8,6 +8,7 @@ from libs import calc_buy_volume
from sdk import OP_BUY from sdk import OP_BUY
from .runtime import Runtime from .runtime import Runtime
from .order import PlaceOrderRequest from .order import PlaceOrderRequest
from .state import PendingOrder
import logging as log import logging as log
@@ -88,6 +89,8 @@ def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float)
signal_key, signal_key,
kind="base", kind="base",
) )
if not run.state.new_order(PendingOrder(order_id, code, "base", volume)):
raise RuntimeError("已有待确认买单")
if not run.orders.place(run.client,request): if not run.orders.place(run.client,request):
raise RuntimeError("订单提交失败") raise RuntimeError("订单提交失败")

View File

@@ -10,6 +10,7 @@ from sdk import OP_BUY, OP_SELL, PositionItem, Tick
from .order import PlaceOrderRequest from .order import PlaceOrderRequest
from .runtime import Runtime from .runtime import Runtime
from .state import PendingOrder
import logging as log import logging as log
LOSS_TIERS = (-30.0, -50.0) LOSS_TIERS = (-30.0, -50.0)
@@ -124,14 +125,13 @@ def handle_profit(
return TradeDecision(False, "无可用整手持仓") return TradeDecision(False, "无可用整手持仓")
order_id = runtime.orders.new_order_id() order_id = runtime.orders.new_order_id()
request = PlaceOrderRequest( request = PlaceOrderRequest(
client=runtime.client,
op=OP_SELL, op=OP_SELL,
code=position.stock_code, code=position.stock_code,
volume=volume, volume=volume,
order_id=order_id, order_id=order_id,
strategy_name=runtime.account_cfg.strategy, strategy_name=runtime.account_cfg.strategy,
) )
if not runtime.orders.place(request): if not runtime.orders.place(runtime.client, request):
return TradeDecision(False, "止盈委托失败") return TradeDecision(False, "止盈委托失败")
return TradeDecision(True, f"卖出 {volume} 股,订单={order_id}") return TradeDecision(True, f"卖出 {volume} 股,订单={order_id}")
@@ -167,7 +167,6 @@ def handle_loss(
order_id = runtime.orders.new_order_id() order_id = runtime.orders.new_order_id()
request = PlaceOrderRequest( request = PlaceOrderRequest(
client=runtime.client,
op=OP_BUY, op=OP_BUY,
code=position.stock_code, code=position.stock_code,
volume=volume, volume=volume,
@@ -175,7 +174,9 @@ def handle_loss(
strategy_name=runtime.account_cfg.strategy, strategy_name=runtime.account_cfg.strategy,
kind="add", kind="add",
) )
if not runtime.orders.place(request): if not runtime.state.new_order(PendingOrder(order_id, position.stock_code, "added", volume)):
return TradeDecision(False, "已有待确认买单")
if not runtime.orders.place(runtime.client, request):
reserved = amount if runtime.state.busy(position.stock_code) else 0.0 reserved = amount if runtime.state.busy(position.stock_code) else 0.0
return TradeDecision(False, "补仓委托失败或待确认", reserved) return TradeDecision(False, "补仓委托失败或待确认", reserved)
runtime.add_watch.forget(position.stock_code) runtime.add_watch.forget(position.stock_code)

View File

@@ -1,13 +1,11 @@
"""简单成交账本:订单明确结束后一次记账,JSON 原子保存。""" """底仓、补仓记录与待确认订单的 JSON 存"""
from __future__ import annotations from __future__ import annotations
import json import json
import logging as log import logging as log
import math import math
import shutil from dataclasses import asdict, dataclass, replace
import time
from dataclasses import asdict, dataclass, field, replace
from pathlib import Path from pathlib import Path
from threading import RLock from threading import RLock
from typing import Iterable from typing import Iterable
@@ -21,26 +19,20 @@ TERMINAL_STATUSES = {"53", "54", "56", "57"}
class StateItem: class StateItem:
code: str code: str
base_order_id: str = "" base_order_id: str = ""
base_pre_qty: int =0
base_qty: int = 0 base_qty: int = 0
base_cost: float = 0.0 base_cost: float = 0.0
added_order_id: str = "" added_order_id: str = ""
added_pre_qty: int =0
added_num: int = 0 added_num: int = 0
added_qty: int = 0 added_qty: int = 0
added_amount: float = 0.0 added_cost: float = 0.0
@property
def added_cost(self) -> float:
return self.added_amount / self.added_qty if self.added_qty else 0.0
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class PendingOrder: class PendingOrder:
order_id: str order_id: str
code: str
kind: str kind: str
expected_qty: int pre_qty: int
submitted_at: float = field(default_factory=time.time)
class State: class State:
@@ -48,154 +40,117 @@ class State:
self.path = Path(path) self.path = Path(path)
self.lock = RLock() self.lock = RLock()
self.items: dict[str, StateItem] = {} self.items: dict[str, StateItem] = {}
self.pending: dict[str, PendingOrder] = {} # 每只证券最多一个待确认买单 self.pending: dict[str, PendingOrder] = {}
self._dirty = False self.IsModify = False
self._next_warning = 0.0
self._load() self._load()
def _load(self) -> None:
"""启动时读取 JSON 中的持仓记录和待确认订单。"""
if self.path.is_file():
raw = json.loads(self.path.read_text(encoding="utf-8"))
self.items = {code: StateItem(**item) for code, item in raw["items"].items()}
self.pending = {code: PendingOrder(**item) for code, item in raw["pending"].items()}
@classmethod @classmethod
def for_strategy(cls, data_dir: str | Path, strategy: str, account_id: str) -> State: def for_strategy(cls, data_dir: str | Path, strategy: str, account_id: str) -> State:
return cls(Path(data_dir) / f"{strategy}_{account_id}_state.json") return cls(Path(data_dir) / f"{strategy}_{account_id}_state.json")
@property
def codes(self) -> list[str]:
with self.lock:
return list(self.items)
def get(self, code: str) -> StateItem: def get(self, code: str) -> StateItem:
with self.lock: with self.lock:
return replace(self.items[code]) return self.items[code]
def busy(self, code: str) -> bool: def busy(self, code: str) -> bool:
with self.lock: with self.lock:
return code in self.pending return code in self.pending
def begin(self, order_id: str, code: str, kind: str, expected_qty: int) -> bool: def new_order(self, order: PendingOrder) -> bool:
if kind not in {"base", "add"} or type(expected_qty) is not int or expected_qty <= 0: """提交前保存待确认订单,同一证券已有 pending 时跳过。"""
raise ValueError("订单类型或预开仓数量无效")
with self.lock: with self.lock:
if code in self.pending or (kind == "base" and code in self.items): if order.code in self.pending:
return False return False
if kind == "add" and code not in self.items: was_modified = self.IsModify
raise ValueError("缺少底仓记录") self.pending[order.code] = order
dirty = self._dirty self.IsModify = True
self.pending[code] = PendingOrder(order_id, kind, expected_qty)
self._dirty = True
try: try:
self.save() # 必须成功落盘后才允许发送请求 self.save()
except Exception: except Exception:
self.pending.pop(code) del self.pending[order.code]
self._dirty = dirty self.IsModify = was_modified
raise raise
return True return True
def reject(self, order_id: str) -> None:
"""仅用于已明确未受理的请求。"""
with self.lock:
for code, pending in self.pending.items():
if pending.order_id == order_id:
del self.pending[code]
self._dirty = True
self.save()
return
def reconcile(self, positions: Iterable[PositionItem], orders: list[OrderItem]) -> None: def reconcile(self, positions: Iterable[PositionItem], orders: list[OrderItem]) -> None:
"""核对 pending 的订单结果,再导入/核对实际持仓。"""
by_id: dict[str, dict[str, OrderItem]] = {} by_id: dict[str, dict[str, OrderItem]] = {}
for order in orders: for order in orders:
if order.local_order_id and order.id: if order.local_order_id:
by_id.setdefault(order.local_order_id, {})[order.id] = order by_id.setdefault(order.local_order_id, {})[order.id] = order
with self.lock:
for position in positions:
code = position.stock_code
if (code and code not in self.items and code not in self.pending
and position.volume > 0 and math.isfinite(position.open_price)
and position.open_price > 0):
self.items[code] = StateItem(code, position.volume, position.open_price)
self._dirty = True
now = time.time() with self.lock:
for code, pending in list(self.pending.items()): for code, pending in list(self.pending.items()):
rows = [o for o in by_id.get(pending.order_id, {}).values() rows = [order for order in by_id.get(pending.order_id, {}).values()
if o.code == code and o.side == "BUY"] if order.code == code and order.side == "BUY"]
filled = _finished_fill(rows, pending.expected_qty) if not rows:
del self.pending[code]
self.IsModify = True
log.info("[状态] 清理无对应订单的 pending代码=%s,订单=%s", code, pending.order_id)
continue
filled = _finished_fill(rows, pending.pre_qty)
if filled is None: if filled is None:
if now >= self._next_warning and now - pending.submitted_at >= 180:
log.warning("[状态] 订单待核查,代码=%s,订单=%s,计划=%d;保留防重",
code, pending.order_id, pending.expected_qty)
continue continue
qty, amount = filled qty, amount = filled
if qty: if qty:
item = self.items.setdefault(code, StateItem(code)) item = self.items.get(code, StateItem(code))
if pending.kind == "base": if pending.kind == "base":
item.base_qty, item.base_cost = qty, amount / qty item = replace(item, base_order_id=pending.order_id,
base_qty=qty, base_cost=amount / qty)
else: else:
item.added_num += 1 total_qty = item.added_qty + qty
item.added_qty += qty total_amount = item.added_qty * item.added_cost + amount
item.added_amount += amount item = replace(item, added_order_id=pending.order_id,
added_num=item.added_num + 1,
added_qty=total_qty, added_cost=total_amount / total_qty)
self.items[code] = item
del self.pending[code] del self.pending[code]
self._dirty = True self.IsModify = True
log.info("[状态] 对账结束,代码=%s,订单=%s,计划=%d,成交=%d", log.info("[状态] 对账结束,代码=%s,订单=%s,计划=%d,成交=%d",
code, pending.order_id, pending.expected_qty, qty) code, pending.order_id, pending.pre_qty, qty)
if now >= self._next_warning:
self._next_warning = now + 180 for position in positions:
self.save() # 成交记账与移除 pending 在同一文件中原子提交 code = position.stock_code
if not code or position.volume <= 0:
continue
if code not in self.items and code not in self.pending:
self.items[code] = StateItem(code=code, base_order_id=position.trade_id,
base_qty=position.volume, base_cost=position.open_price)
self.IsModify = True
elif code in self.items:
item = self.items[code]
if item.base_qty + item.added_qty != position.volume:
log.info("[状态] 持仓数量差异,代码=%s,记录=%d,实际=%d",
code, item.base_qty + item.added_qty, position.volume)
self.save()
def save(self) -> None: def save(self) -> None:
with self.lock: with self.lock:
if not self._dirty: if not self.IsModify:
return return
self.path.parent.mkdir(parents=True, exist_ok=True) self.path.parent.mkdir(parents=True, exist_ok=True)
temporary = self.path.with_suffix(self.path.suffix + ".tmp") temporary = self.path.with_suffix(self.path.suffix + ".tmp")
payload = dict(version=3, items={k: asdict(v) for k, v in self.items.items()}, payload = dict(items={code: asdict(item) for code, item in self.items.items()},
pending={k: asdict(v) for k, v in self.pending.items()}) pending={code: asdict(item) for code, item in self.pending.items()})
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8") temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8")
temporary.replace(self.path) temporary.replace(self.path)
self._dirty = False self.IsModify = False
def _load(self) -> None:
try:
raw = json.loads(self.path.read_text(encoding="utf-8"))
except FileNotFoundError:
return
if raw.get("version") not in {2, 3}:
raise ValueError("仅支持 v2/v3 状态文件;旧格式请先核实转换,原文件未修改")
self.items = {k: StateItem(**v) for k, v in raw["items"].items()}
if raw["version"] == 3:
self.pending = {k: PendingOrder(**v) for k, v in raw["pending"].items()}
return
# v2 的未决订单可能已经增量记账,先撤回这部分,结束时再完整记一次。
for old in raw["pending"].values():
code = old["code"]
if code in self.pending:
raise ValueError(f"{code} 存在多个旧未决订单,请核查;原文件未修改")
item = self.items[code]
qty, amount = old.get("applied_qty", 0), old.get("applied_amount", 0.0)
if old["kind"] == "add":
item.added_qty -= qty
item.added_amount -= amount
item.added_num -= int(old.get("counted", False))
else:
base_amount = item.base_qty * item.base_cost - amount
item.base_qty -= qty
item.base_cost = base_amount / item.base_qty if item.base_qty else 0.0
if not item.base_qty and not item.added_qty:
del self.items[code]
self.pending[code] = PendingOrder(old["order_id"], old["kind"], old["expected_qty"],
old.get("submitted_at", time.time()))
backup = self.path.with_suffix(self.path.suffix + ".v2.bak")
if not backup.exists():
shutil.copy2(self.path, backup)
self._dirty = True
self.save()
def _finished_fill(orders: list[OrderItem], expected_qty: int) -> tuple[int, float] | None: def _finished_fill(orders: list[OrderItem], pre_qty: int) -> tuple[int, float] | None:
"""仅完整终态快照可记账;缺项、未成交完或金额未知均继续等待""" """订单全部结束且成交金额完整后一次记账,部分成交撤单也按实计入"""
if not orders or expected_qty <= 0 or sum(o.volume for o in orders) != expected_qty: if sum(order.volume for order in orders) != pre_qty:
return None return None
qty, amount = 0, 0.0 qty, amount = 0, 0.0
for order in orders: for order in orders:
if (order.status not in TERMINAL_STATUSES or not 0 <= order.traded_volume <= order.volume if (order.status not in TERMINAL_STATUSES
or (order.status == "56" and order.traded_volume != order.volume)): or (order.status == "56" and order.traded_volume != order.volume)):
return None return None
if order.traded_volume: if order.traded_volume: