fix bug
This commit is contained in:
@@ -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("订单提交失败")
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
"""判断证券是否存在仍在处理中的同方向委托。"""
|
"""判断证券是否存在仍在处理中的同方向委托。"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.pending[order.code] = order
|
|
||||||
self.IsModify = True
|
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)
|
||||||
|
if item is None:
|
||||||
|
if code not in pending_codes:
|
||||||
self.items[code] = StateItem(code=code, base_order_id=position.trade_id,
|
self.items[code] = StateItem(code=code, base_order_id=position.trade_id,
|
||||||
base_qty=position.volume, base_cost=position.open_price)
|
base_qty=position.volume, base_cost=position.open_price)
|
||||||
self.IsModify = True
|
self.IsModify = True
|
||||||
elif code in self.items:
|
continue
|
||||||
item = self.items[code]
|
|
||||||
if 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",
|
log.info("[状态] 持仓数量差异,代码=%s,记录=%d,实际=%d",
|
||||||
code, item.base_qty + item.added_qty, position.volume)
|
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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user