This commit is contained in:
2026-09-05 20:39:58 +08:00
parent 1fc3e119d3
commit af55324e2a
4 changed files with 110 additions and 80 deletions

View File

@@ -80,7 +80,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None:
def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float) -> None: def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float) -> None:
"""生成本地订单号并按最新价提交开仓委托。""" """生成本地订单号并按最新价提交开仓委托。"""
order_id = run.orders.new_order_id() order_id = run.orders.new_order_id("BUY")
request = PlaceOrderRequest( request = PlaceOrderRequest(
OP_BUY, OP_BUY,
code, code,
@@ -89,8 +89,9 @@ 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("已有待确认买单") #run.state.new_order(PendingOrder(order_id, code, "base", volume))
if not run.orders.place(run.client,request): if not run.orders.place(run.client,request):
raise RuntimeError("订单提交失败") raise RuntimeError("订单提交失败")

View File

@@ -42,9 +42,9 @@ class OrderBook:
self.mutex = Lock() self.mutex = Lock()
@staticmethod @staticmethod
def new_order_id() -> str: def new_order_id(side:str) -> str:
"""生成 ``trend-xxxxxxxx`` 格式的本地订单号。""" """生成 ``trend-xxxxxxxx`` 格式的本地订单号。"""
return f"trend-{secrets.token_hex(12)}" return f"trend-{side}-{secrets.token_hex(10)}"
def busy(self, code: str, side: str) -> bool: def busy(self, code: str, side: str) -> bool:
"""判断证券是否存在仍在处理中的同方向委托。""" """判断证券是否存在仍在处理中的同方向委托。"""

View File

@@ -123,7 +123,7 @@ def handle_profit(
volume = position.can_use_volume - position.can_use_volume % 100 volume = position.can_use_volume - position.can_use_volume % 100
if volume <= 0: if volume <= 0:
return TradeDecision(False, "无可用整手持仓") return TradeDecision(False, "无可用整手持仓")
order_id = runtime.orders.new_order_id() order_id = runtime.orders.new_order_id("SELL")
request = PlaceOrderRequest( request = PlaceOrderRequest(
op=OP_SELL, op=OP_SELL,
code=position.stock_code, code=position.stock_code,
@@ -165,7 +165,7 @@ def handle_loss(
if volume <= 0 or amount > available: if volume <= 0 or amount > available:
return TradeDecision(False, "本轮可用资金不足") return TradeDecision(False, "本轮可用资金不足")
order_id = runtime.orders.new_order_id() order_id = runtime.orders.new_order_id("BUY")
request = PlaceOrderRequest( request = PlaceOrderRequest(
op=OP_BUY, op=OP_BUY,
code=position.stock_code, code=position.stock_code,
@@ -174,8 +174,9 @@ def handle_loss(
strategy_name=runtime.account_cfg.strategy, strategy_name=runtime.account_cfg.strategy,
kind="add", kind="add",
) )
if not runtime.state.new_order(PendingOrder(order_id, position.stock_code, "added", volume)):
return TradeDecision(False, "已有待确认买单") #runtime.state.new_order(PendingOrder(order_id, position.stock_code, "added", volume))
if not runtime.orders.place(runtime.client, request): 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)

View File

@@ -4,15 +4,15 @@ from __future__ import annotations
import json import json
import logging as log import logging as log
import math import time
from dataclasses import asdict, dataclass, replace 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 Any
from sdk import OrderItem, PositionItem from sdk import OrderItem, PositionItem
TERMINAL_STATUSES = {"53", "54", "56", "57"} PENDING_TIME_OUT = 3600
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@@ -33,14 +33,15 @@ class PendingOrder:
code: str code: str
kind: str kind: str
pre_qty: int pre_qty: int
submit_at: int = field(default_factory=lambda: int(time.time()))
class State: class State:
def __init__(self, path: str | Path) -> None: def __init__(self, data_dir: str | Path, strategy: str, account_id: str) -> None:
self.path = Path(path) self.path = Path(data_dir) / f"{strategy}_{account_id}_state.json"
self.lock = RLock() self.lock = RLock()
self.items: dict[str, StateItem] = {} self.items: dict[str, StateItem] = {}
self.pending: dict[str, PendingOrder] = {} self.pending: list[PendingOrder] = []
self.IsModify = False self.IsModify = False
self._load() self._load()
@@ -49,86 +50,131 @@ class State:
if self.path.is_file(): if self.path.is_file():
raw = json.loads(self.path.read_text(encoding="utf-8")) raw = json.loads(self.path.read_text(encoding="utf-8"))
self.items = {code: StateItem(**item) for code, item in raw["items"].items()} self.items = {code: StateItem(**item) for code, item in raw["items"].items()}
self.pending = {code: PendingOrder(**item) for code, item in raw["pending"].items()} 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()
@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")
def get(self, code: str) -> StateItem: def get(self, code: str) -> StateItem:
with self.lock: with self.lock:
return self.items[code] return self.items[code]
def busy(self, code: str) -> bool: def new_order(self, order: PendingOrder) -> None:
with self.lock:
return code in self.pending
def new_order(self, order: PendingOrder) -> bool:
"""提交前保存待确认订单,同一证券已有 pending 时跳过。""" """提交前保存待确认订单,同一证券已有 pending 时跳过。"""
with self.lock: with self.lock:
if order.code in self.pending: if order:
return False self.pending.append(order)
was_modified = self.IsModify self.IsModify = True
self.pending[order.code] = order
self.IsModify = True
try:
self.save() self.save()
except Exception:
del self.pending[order.code]
self.IsModify = was_modified
raise
return True
def reconcile(self, positions: Iterable[PositionItem], orders: list[OrderItem]) -> None: def merged_order(self, orders: list[OrderItem]) -> dict[str, dict[str, Any]]:
"""核对 pending 的订单结果,再导入/核对实际持仓""" """按证券代码、本地订单号合并,返回成交数量、金额、均价和状态"""
by_id: dict[str, dict[str, OrderItem]] = {} merged: dict[str, dict[str, Any]] = {}
for order in orders: for order in orders:
if order.local_order_id: if not order.local_order_id:
by_id.setdefault(order.local_order_id, {})[order.id] = order 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)
with self.lock: with self.lock:
for code, pending in list(self.pending.items()): # 2. 按本地订单号查找合并结果,核对证券代码后写入底仓或补仓。
rows = [order for order in by_id.get(pending.order_id, {}).values() # 只留下尚未完成确认的订单,避免循环中反复查找、删除列表元素。
if order.code == code and order.side == "BUY"] remaining: list[PendingOrder] = []
if not rows: for pending in self.pending:
del self.pending[code] code = pending.code
result = merged.get(pending.order_id)
if result is None or result["code"] != code:
# 按现有规则:本轮快照中没有对应订单,就清理 pending。
self.IsModify = True self.IsModify = True
log.info("[状态] 清理无对应订单的 pending代码=%s,订单=%s", code, pending.order_id) log.info("[状态] 清理无对应订单的 pending代码=%s,订单=%s", code, pending.order_id)
continue continue
filled = _finished_fill(rows, pending.pre_qty) if result["merged_status"] != "OK" or result["qty"] != pending.pre_qty:
if filled is None: # 未全部成功,或拆单快照的数量尚未齐全,留到下轮确认。
remaining.append(pending)
continue continue
qty, amount = filled qty, cost = result["qty"], result["cost"]
# 只记录实际成交。
if qty: if qty:
item = self.items.get(code, StateItem(code)) item = self.items.get(code, StateItem(code))
if pending.kind == "base": if pending.kind == "base":
# 底仓:记录本次成交数量和实际均价。
item = replace(item, base_order_id=pending.order_id, item = replace(item, base_order_id=pending.order_id,
base_qty=qty, base_cost=amount / qty) base_qty=qty, base_cost=cost)
else: else:
# 补仓:次数加一,数量累加,成本按成交数量加权。
total_qty = item.added_qty + qty total_qty = item.added_qty + qty
total_amount = item.added_qty * item.added_cost + amount total_amount = item.added_qty * item.added_cost + qty * cost
item = replace(item, added_order_id=pending.order_id, item = replace(item, added_order_id=pending.order_id,
added_num=item.added_num + 1, added_num=item.added_num + 1,
added_qty=total_qty, added_cost=total_amount / total_qty) added_qty=total_qty, added_cost=total_amount / total_qty)
self.items[code] = item self.items[code] = item
del self.pending[code]
self.IsModify = True self.IsModify = True
log.info("[状态] 对账结束,代码=%s,订单=%s,计划=%d,成交=%d", log.info("[状态] 对账结束,代码=%s,订单=%s,计划=%d,成交=%d",
code, pending.order_id, pending.pre_qty, qty) code, pending.order_id, pending.pre_qty, qty)
self.pending = remaining
# 未记录且没有待确认订单的持仓,作为首次接管的底仓导入。
pending_codes = {pending.code for pending in remaining}
for position in positions: for position in positions:
code = position.stock_code code = position.stock_code
if not code or position.volume <= 0: if not code or position.volume <= 0:
continue continue
if code not in self.items and code not in self.pending: item = self.items.get(code)
self.items[code] = StateItem(code=code, base_order_id=position.trade_id, if item is None:
base_qty=position.volume, base_cost=position.open_price) if code not in pending_codes:
self.IsModify = True self.items[code] = StateItem(code=code, base_order_id=position.trade_id,
elif code in self.items: base_qty=position.volume, base_cost=position.open_price)
item = self.items[code] self.IsModify = True
if item.base_qty + item.added_qty != position.volume: continue
log.info("[状态] 持仓数量差异,代码=%s,记录=%d,实际=%d",
code, item.base_qty + item.added_qty, position.volume) # 已有记录只报告数量差异,不用总持仓覆盖底仓/补仓的划分。
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() 不写文件。
self.save() self.save()
def save(self) -> None: def save(self) -> None:
@@ -138,25 +184,7 @@ class State:
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(items={code: asdict(item) for code, item in self.items.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()}) pending=[asdict(item) for item in self.pending])
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.IsModify = False self.IsModify = False
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 (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