2026-09-05 15:56:39 +08:00
|
|
|
|
"""底仓、补仓记录与待确认订单的 JSON 存储。"""
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
2026-09-01 14:28:49 +08:00
|
|
|
|
import logging as log
|
2026-09-05 13:53:26 +08:00
|
|
|
|
import math
|
2026-09-05 15:56:39 +08:00
|
|
|
|
from dataclasses import asdict, dataclass, replace
|
2026-08-28 18:52:27 +08:00
|
|
|
|
from pathlib import Path
|
2026-09-05 13:53:26 +08:00
|
|
|
|
from threading import RLock
|
2026-08-28 18:52:27 +08:00
|
|
|
|
from typing import Iterable
|
|
|
|
|
|
|
2026-08-30 00:34:27 +08:00
|
|
|
|
from sdk import OrderItem, PositionItem
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
2026-09-05 13:53:26 +08:00
|
|
|
|
TERMINAL_STATUSES = {"53", "54", "56", "57"}
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-05 13:56:04 +08:00
|
|
|
|
@dataclass(frozen=True, slots=True)
|
2026-08-28 18:52:27 +08:00
|
|
|
|
class StateItem:
|
|
|
|
|
|
code: str
|
|
|
|
|
|
base_order_id: str = ""
|
|
|
|
|
|
base_qty: int = 0
|
|
|
|
|
|
base_cost: float = 0.0
|
|
|
|
|
|
added_order_id: str = ""
|
|
|
|
|
|
added_num: int = 0
|
|
|
|
|
|
added_qty: int = 0
|
2026-09-05 15:56:39 +08:00
|
|
|
|
added_cost: float = 0.0
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-05 13:56:04 +08:00
|
|
|
|
@dataclass(frozen=True, slots=True)
|
2026-09-05 13:53:26 +08:00
|
|
|
|
class PendingOrder:
|
|
|
|
|
|
order_id: str
|
2026-09-05 15:56:39 +08:00
|
|
|
|
code: str
|
2026-09-05 13:53:26 +08:00
|
|
|
|
kind: str
|
2026-09-05 15:56:39 +08:00
|
|
|
|
pre_qty: int
|
2026-09-05 13:53:26 +08:00
|
|
|
|
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
2026-09-05 13:53:26 +08:00
|
|
|
|
class State:
|
2026-08-28 18:52:27 +08:00
|
|
|
|
def __init__(self, path: str | Path) -> None:
|
|
|
|
|
|
self.path = Path(path)
|
2026-09-05 13:53:26 +08:00
|
|
|
|
self.lock = RLock()
|
|
|
|
|
|
self.items: dict[str, StateItem] = {}
|
2026-09-05 15:56:39 +08:00
|
|
|
|
self.pending: dict[str, PendingOrder] = {}
|
|
|
|
|
|
self.IsModify = False
|
2026-09-05 13:53:26 +08:00
|
|
|
|
self._load()
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
2026-09-05 15:56:39 +08:00
|
|
|
|
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()}
|
|
|
|
|
|
|
2026-08-28 18:52:27 +08:00
|
|
|
|
@classmethod
|
2026-09-05 13:53:26 +08:00
|
|
|
|
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")
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
|
|
|
|
|
def get(self, code: str) -> StateItem:
|
|
|
|
|
|
with self.lock:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
return self.items[code]
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
2026-09-05 13:53:26 +08:00
|
|
|
|
def busy(self, code: str) -> bool:
|
2026-08-28 18:52:27 +08:00
|
|
|
|
with self.lock:
|
2026-09-05 13:53:26 +08:00
|
|
|
|
return code in self.pending
|
2026-08-28 18:52:27 +08:00
|
|
|
|
|
2026-09-05 15:56:39 +08:00
|
|
|
|
def new_order(self, order: PendingOrder) -> bool:
|
|
|
|
|
|
"""提交前保存待确认订单,同一证券已有 pending 时跳过。"""
|
2026-08-30 00:34:27 +08:00
|
|
|
|
with self.lock:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
if order.code in self.pending:
|
2026-09-05 13:53:26 +08:00
|
|
|
|
return False
|
2026-09-05 15:56:39 +08:00
|
|
|
|
was_modified = self.IsModify
|
|
|
|
|
|
self.pending[order.code] = order
|
|
|
|
|
|
self.IsModify = True
|
2026-09-05 13:53:26 +08:00
|
|
|
|
try:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
self.save()
|
2026-09-05 13:53:26 +08:00
|
|
|
|
except Exception:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
del self.pending[order.code]
|
|
|
|
|
|
self.IsModify = was_modified
|
2026-09-05 13:53:26 +08:00
|
|
|
|
raise
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def reconcile(self, positions: Iterable[PositionItem], orders: list[OrderItem]) -> None:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
"""核对 pending 的订单结果,再导入/核对实际持仓。"""
|
2026-09-05 13:53:26 +08:00
|
|
|
|
by_id: dict[str, dict[str, OrderItem]] = {}
|
2026-08-30 15:29:21 +08:00
|
|
|
|
for order in orders:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
if order.local_order_id:
|
2026-09-05 13:53:26 +08:00
|
|
|
|
by_id.setdefault(order.local_order_id, {})[order.id] = order
|
|
|
|
|
|
|
2026-09-05 15:56:39 +08:00
|
|
|
|
with self.lock:
|
2026-09-05 13:53:26 +08:00
|
|
|
|
for code, pending in list(self.pending.items()):
|
2026-09-05 15:56:39 +08:00
|
|
|
|
rows = [order for order in by_id.get(pending.order_id, {}).values()
|
|
|
|
|
|
if order.code == code and order.side == "BUY"]
|
|
|
|
|
|
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)
|
2026-09-05 13:53:26 +08:00
|
|
|
|
if filled is None:
|
2026-08-30 15:29:21 +08:00
|
|
|
|
continue
|
2026-09-05 13:53:26 +08:00
|
|
|
|
qty, amount = filled
|
|
|
|
|
|
if qty:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
item = self.items.get(code, StateItem(code))
|
2026-09-05 13:53:26 +08:00
|
|
|
|
if pending.kind == "base":
|
2026-09-05 15:56:39 +08:00
|
|
|
|
item = replace(item, base_order_id=pending.order_id,
|
|
|
|
|
|
base_qty=qty, base_cost=amount / qty)
|
2026-09-05 13:53:26 +08:00
|
|
|
|
else:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
total_qty = item.added_qty + qty
|
|
|
|
|
|
total_amount = item.added_qty * item.added_cost + 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
|
2026-09-05 13:53:26 +08:00
|
|
|
|
del self.pending[code]
|
2026-09-05 15:56:39 +08:00
|
|
|
|
self.IsModify = True
|
2026-09-05 13:53:26 +08:00
|
|
|
|
log.info("[状态] 对账结束,代码=%s,订单=%s,计划=%d,成交=%d",
|
2026-09-05 15:56:39 +08:00
|
|
|
|
code, pending.order_id, pending.pre_qty, qty)
|
|
|
|
|
|
|
|
|
|
|
|
for position in positions:
|
|
|
|
|
|
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()
|
2026-08-28 22:46:04 +08:00
|
|
|
|
|
2026-08-28 18:52:27 +08:00
|
|
|
|
def save(self) -> None:
|
|
|
|
|
|
with self.lock:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
if not self.IsModify:
|
2026-09-05 13:53:26 +08:00
|
|
|
|
return
|
2026-08-28 18:52:27 +08:00
|
|
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
2026-09-05 13:53:26 +08:00
|
|
|
|
temporary = self.path.with_suffix(self.path.suffix + ".tmp")
|
2026-09-05 15:56:39 +08:00
|
|
|
|
payload = dict(items={code: asdict(item) for code, item in self.items.items()},
|
|
|
|
|
|
pending={code: asdict(item) for code, item in self.pending.items()})
|
2026-09-05 13:53:26 +08:00
|
|
|
|
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8")
|
|
|
|
|
|
temporary.replace(self.path)
|
2026-09-05 15:56:39 +08:00
|
|
|
|
self.IsModify = False
|
2026-09-05 13:53:26 +08:00
|
|
|
|
|
2026-09-05 15:56:39 +08:00
|
|
|
|
|
|
|
|
|
|
def _finished_fill(orders: list[OrderItem], pre_qty: int) -> tuple[int, float] | None:
|
|
|
|
|
|
"""订单全部结束且成交金额完整后一次记账,部分成交撤单也按实计入。"""
|
|
|
|
|
|
if sum(order.volume for order in orders) != pre_qty:
|
2026-09-05 13:53:26 +08:00
|
|
|
|
return None
|
|
|
|
|
|
qty, amount = 0, 0.0
|
|
|
|
|
|
for order in orders:
|
2026-09-05 15:56:39 +08:00
|
|
|
|
if (order.status not in TERMINAL_STATUSES
|
2026-09-05 13:53:26 +08:00
|
|
|
|
or (order.status == "56" and order.traded_volume != order.volume)):
|
|
|
|
|
|
return None
|
|
|
|
|
|
if order.traded_volume:
|
|
|
|
|
|
value = order.trade_amount if order.trade_amount > 0 else order.trade_price * order.traded_volume
|
|
|
|
|
|
if not math.isfinite(value) or value <= 0:
|
|
|
|
|
|
return None
|
|
|
|
|
|
qty += order.traded_volume
|
|
|
|
|
|
amount += value
|
|
|
|
|
|
return qty, amount
|