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

View File

@@ -10,6 +10,7 @@ from sdk import OP_BUY, OP_SELL, PositionItem, Tick
from .order import PlaceOrderRequest
from .runtime import Runtime
from .state import PendingOrder
import logging as log
LOSS_TIERS = (-30.0, -50.0)
@@ -124,14 +125,13 @@ def handle_profit(
return TradeDecision(False, "无可用整手持仓")
order_id = runtime.orders.new_order_id()
request = PlaceOrderRequest(
client=runtime.client,
op=OP_SELL,
code=position.stock_code,
volume=volume,
order_id=order_id,
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(True, f"卖出 {volume} 股,订单={order_id}")
@@ -167,7 +167,6 @@ def handle_loss(
order_id = runtime.orders.new_order_id()
request = PlaceOrderRequest(
client=runtime.client,
op=OP_BUY,
code=position.stock_code,
volume=volume,
@@ -175,7 +174,9 @@ def handle_loss(
strategy_name=runtime.account_cfg.strategy,
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
return TradeDecision(False, "补仓委托失败或待确认", reserved)
runtime.add_watch.forget(position.stock_code)

View File

@@ -1,13 +1,11 @@
"""简单成交账本:订单明确结束后一次记账,JSON 原子保存。"""
"""底仓、补仓记录与待确认订单的 JSON 存"""
from __future__ import annotations
import json
import logging as log
import math
import shutil
import time
from dataclasses import asdict, dataclass, field, replace
from dataclasses import asdict, dataclass, replace
from pathlib import Path
from threading import RLock
from typing import Iterable
@@ -21,26 +19,20 @@ TERMINAL_STATUSES = {"53", "54", "56", "57"}
class StateItem:
code: str
base_order_id: str = ""
base_pre_qty: int =0
base_qty: int = 0
base_cost: float = 0.0
added_order_id: str = ""
added_pre_qty: int =0
added_num: int = 0
added_qty: int = 0
added_amount: float = 0.0
@property
def added_cost(self) -> float:
return self.added_amount / self.added_qty if self.added_qty else 0.0
added_cost: float = 0.0
@dataclass(frozen=True, slots=True)
class PendingOrder:
order_id: str
code: str
kind: str
expected_qty: int
submitted_at: float = field(default_factory=time.time)
pre_qty: int
class State:
@@ -48,154 +40,117 @@ class State:
self.path = Path(path)
self.lock = RLock()
self.items: dict[str, StateItem] = {}
self.pending: dict[str, PendingOrder] = {} # 每只证券最多一个待确认买单
self._dirty = False
self._next_warning = 0.0
self.pending: dict[str, PendingOrder] = {}
self.IsModify = False
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
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")
@property
def codes(self) -> list[str]:
with self.lock:
return list(self.items)
def get(self, code: str) -> StateItem:
with self.lock:
return replace(self.items[code])
return self.items[code]
def busy(self, code: str) -> bool:
with self.lock:
return code in self.pending
def begin(self, order_id: str, code: str, kind: str, expected_qty: int) -> bool:
if kind not in {"base", "add"} or type(expected_qty) is not int or expected_qty <= 0:
raise ValueError("订单类型或预开仓数量无效")
def new_order(self, order: PendingOrder) -> bool:
"""提交前保存待确认订单,同一证券已有 pending 时跳过。"""
with self.lock:
if code in self.pending or (kind == "base" and code in self.items):
if order.code in self.pending:
return False
if kind == "add" and code not in self.items:
raise ValueError("缺少底仓记录")
dirty = self._dirty
self.pending[code] = PendingOrder(order_id, kind, expected_qty)
self._dirty = True
was_modified = self.IsModify
self.pending[order.code] = order
self.IsModify = True
try:
self.save() # 必须成功落盘后才允许发送请求
self.save()
except Exception:
self.pending.pop(code)
self._dirty = dirty
del self.pending[order.code]
self.IsModify = was_modified
raise
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:
"""核对 pending 的订单结果,再导入/核对实际持仓。"""
by_id: dict[str, dict[str, OrderItem]] = {}
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
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()):
rows = [o for o in by_id.get(pending.order_id, {}).values()
if o.code == code and o.side == "BUY"]
filled = _finished_fill(rows, pending.expected_qty)
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)
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
qty, amount = filled
if qty:
item = self.items.setdefault(code, StateItem(code))
item = self.items.get(code, StateItem(code))
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:
item.added_num += 1
item.added_qty += qty
item.added_amount += amount
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
del self.pending[code]
self._dirty = True
self.IsModify = True
log.info("[状态] 对账结束,代码=%s,订单=%s,计划=%d,成交=%d",
code, pending.order_id, pending.expected_qty, qty)
if now >= self._next_warning:
self._next_warning = now + 180
self.save() # 成交记账与移除 pending 在同一文件中原子提交
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()
def save(self) -> None:
with self.lock:
if not self._dirty:
if not self.IsModify:
return
self.path.parent.mkdir(parents=True, exist_ok=True)
temporary = self.path.with_suffix(self.path.suffix + ".tmp")
payload = dict(version=3, items={k: asdict(v) for k, v in self.items.items()},
pending={k: asdict(v) for k, v in self.pending.items()})
payload = dict(items={code: asdict(item) for code, item in self.items.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.replace(self.path)
self._dirty = 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()
self.IsModify = False
def _finished_fill(orders: list[OrderItem], expected_qty: int) -> tuple[int, float] | None:
"""仅完整终态快照可记账;缺项、未成交完或金额未知均继续等待"""
if not orders or expected_qty <= 0 or sum(o.volume for o in orders) != expected_qty:
def _finished_fill(orders: list[OrderItem], pre_qty: int) -> tuple[int, float] | None:
"""订单全部结束且成交金额完整后一次记账,部分成交撤单也按实计入"""
if sum(order.volume for order in orders) != pre_qty:
return None
qty, amount = 0, 0.0
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)):
return None
if order.traded_volume: