Files
big-qmt/docs/arch/state.py

191 lines
8.1 KiB
Python
Raw Normal View History

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 20:39:58 +08:00
import time
from dataclasses import asdict, dataclass, field, 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-09-05 20:39:58 +08:00
from typing import Any
2026-08-28 18:52:27 +08:00
2026-08-30 00:34:27 +08:00
from sdk import OrderItem, PositionItem
2026-08-28 18:52:27 +08:00
2026-09-05 20:39:58 +08:00
PENDING_TIME_OUT = 3600
2026-08-28 18:52:27 +08:00
2026-09-06 11:34:23 +08:00
@dataclass(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-06 11:34:23 +08:00
@dataclass(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 20:39:58 +08:00
submit_at: int = field(default_factory=lambda: int(time.time()))
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-09-05 20:39:58 +08:00
def __init__(self, data_dir: str | Path, strategy: str, account_id: str) -> None:
self.path = Path(data_dir) / f"{strategy}_{account_id}_state.json"
2026-09-05 13:53:26 +08:00
self.lock = RLock()
self.items: dict[str, StateItem] = {}
2026-09-05 20:39:58 +08:00
self.pending: list[PendingOrder] = []
2026-09-05 15:56:39 +08:00
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()}
2026-09-05 20:39:58 +08:00
self.pending = [PendingOrder(**item) for item in raw["pending"]]
now = int(time.time())
for pending in list(self.pending):
if now - pending.submit_at >= PENDING_TIME_OUT:
self.pending.remove(pending)
self.IsModify = True
log.info("[状态] 清理超时 pending代码=%s,订单=%s", pending.code, pending.order_id)
self.save()
2026-09-05 15:56:39 +08:00
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 20:39:58 +08:00
def new_order(self, order: PendingOrder) -> None:
2026-09-05 15:56:39 +08:00
"""提交前保存待确认订单,同一证券已有 pending 时跳过。"""
2026-08-30 00:34:27 +08:00
with self.lock:
2026-09-05 20:39:58 +08:00
if order:
self.pending.append(order)
self.IsModify = True
2026-09-05 15:56:39 +08:00
self.save()
2026-09-05 20:39:58 +08:00
def merged_order(self, orders: list[OrderItem]) -> dict[str, dict[str, Any]]:
"""按证券代码、本地订单号合并,返回成交数量、金额、均价和状态。"""
merged: dict[str, dict[str, Any]] = {}
2026-08-30 15:29:21 +08:00
for order in orders:
2026-09-05 20:39:58 +08:00
if not order.local_order_id:
continue
if not order.local_order_id in merged.keys():
strStatus = "ING"
if order.status == "56":
strStatus = "OK"
merged[order.local_order_id] = {
"code":order.code,
"qty": order.traded_volume,
"cost": order.trade_price,
"status": order.status,
"merged_status":strStatus
}
continue
old = merged[order.local_order_id]
strStatus = "ING"
if old["status"] == order.status == "56":
strStatus = "OK"
totalQty = old["qty"]+order.traded_volume
# 合计成交数量为零,跳过,避免除零。
if totalQty == 0:
continue
# 有成交数量但缺少成交金额,跳过,避免拉低成本。
if order.traded_volume > 0 and not order.trade_amount:
continue
cost = ((old["qty"]*old["cost"])+order.trade_amount) / totalQty
merged[order.local_order_id]["qty"] = totalQty
merged[order.local_order_id]["cost"] = cost
merged[order.local_order_id]["merged_status"] = strStatus
return merged
def reconcile(self, positions: list[PositionItem], orders: list[OrderItem]) -> None:
"""合并订单 → 对齐 pending 和持仓 → 保存 JSON。"""
# 1. 合并同一本地订单的成交数据。
merged = self.merged_order(orders)
2026-09-05 13:53:26 +08:00
2026-09-05 15:56:39 +08:00
with self.lock:
2026-09-05 20:39:58 +08:00
# 2. 按本地订单号查找合并结果,核对证券代码后写入底仓或补仓。
# 只留下尚未完成确认的订单,避免循环中反复查找、删除列表元素。
remaining: list[PendingOrder] = []
for pending in self.pending:
code = pending.code
result = merged.get(pending.order_id)
if result is None or result["code"] != code:
# 按现有规则:本轮快照中没有对应订单,就清理 pending。
2026-09-05 15:56:39 +08:00
self.IsModify = True
log.info("[状态] 清理无对应订单的 pending代码=%s,订单=%s", code, pending.order_id)
continue
2026-09-05 20:39:58 +08:00
if result["merged_status"] != "OK" or result["qty"] != pending.pre_qty:
# 未全部成功,或拆单快照的数量尚未齐全,留到下轮确认。
remaining.append(pending)
2026-08-30 15:29:21 +08:00
continue
2026-09-05 20:39:58 +08:00
qty, cost = result["qty"], result["cost"]
# 只记录实际成交。
2026-09-05 13:53:26 +08:00
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 20:39:58 +08:00
# 底仓:记录本次成交数量和实际均价。
2026-09-05 15:56:39 +08:00
item = replace(item, base_order_id=pending.order_id,
2026-09-05 20:39:58 +08:00
base_qty=qty, base_cost=cost)
2026-09-05 13:53:26 +08:00
else:
2026-09-05 20:39:58 +08:00
# 补仓:次数加一,数量累加,成本按成交数量加权。
2026-09-05 15:56:39 +08:00
total_qty = item.added_qty + qty
2026-09-05 20:39:58 +08:00
total_amount = item.added_qty * item.added_cost + qty * cost
2026-09-05 15:56:39 +08:00
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
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)
2026-09-05 20:39:58 +08:00
self.pending = remaining
# 未记录且没有待确认订单的持仓,作为首次接管的底仓导入。
pending_codes = {pending.code for pending in remaining}
2026-09-05 15:56:39 +08:00
for position in positions:
code = position.stock_code
if not code or position.volume <= 0:
continue
2026-09-05 20:39:58 +08:00
item = self.items.get(code)
if item is None:
if code not in pending_codes:
self.items[code] = StateItem(code=code, base_order_id=position.trade_id,
base_qty=position.volume, base_cost=position.open_price)
self.IsModify = True
continue
# 已有记录只报告数量差异,不用总持仓覆盖底仓/补仓的划分。
recorded_qty = item.base_qty + item.added_qty
if recorded_qty != position.volume:
log.info("[状态] 持仓数量差异,代码=%s,记录=%d,实际=%d",
code, recorded_qty, position.volume)
# 3. 本轮统一保存;没有修改时 save() 不写文件。
2026-09-05 15:56:39 +08:00
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()},
2026-09-05 20:39:58 +08:00
pending=[asdict(item) for item in self.pending])
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