fix bug
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from .client import Client
|
||||
from .errors import APIError, BusinessError
|
||||
from .models import Assets, OrderItem, Portfolio, PositionItem, Tick
|
||||
from .models import Assets, DealItem, OrderItem, Portfolio, PositionItem, Tick
|
||||
from .trade import (
|
||||
OP_BUY,
|
||||
OP_SELL,
|
||||
@@ -9,11 +9,13 @@ from .trade import (
|
||||
PR_TYPE_LATEST,
|
||||
QUICK_TRADE_NOW,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"APIError",
|
||||
"Assets",
|
||||
"BusinessError",
|
||||
"Client",
|
||||
"DealItem",
|
||||
"OP_BUY",
|
||||
"OP_SELL",
|
||||
"ORDER_SIDE_BY_OFFSET",
|
||||
|
||||
@@ -14,6 +14,7 @@ def _number(value: Any, kind: type = float) -> Any:
|
||||
@dataclass(slots=True)
|
||||
class OrderItem:
|
||||
"""由 QMT 委托明细解析得到的标准订单记录。"""
|
||||
|
||||
id: str
|
||||
code: str
|
||||
side: str
|
||||
@@ -64,11 +65,74 @@ class OrderItem:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DealItem:
|
||||
"""由 QMT Deal 成交对象解析得到的标准成交记录。"""
|
||||
|
||||
id: str
|
||||
order_id: str
|
||||
code: str
|
||||
side: str
|
||||
remark: str
|
||||
traded_at: datetime | None
|
||||
volume: int
|
||||
price: float
|
||||
amount: float
|
||||
local_order_id: str = ""
|
||||
order_ref: str = ""
|
||||
exchange_id: str = ""
|
||||
name: str = ""
|
||||
account_id: str = ""
|
||||
commission: float = 0.0
|
||||
trade_date: str = ""
|
||||
trade_time: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_trade_detail(cls, data: dict[str, Any]) -> "DealItem":
|
||||
"""从 TradeDetailData 的 QMT Deal 原始字段创建成交记录。"""
|
||||
instrument_id = str(data.get("m_strInstrumentID") or "")
|
||||
exchange_id = str(data.get("m_strExchangeID") or "")
|
||||
code = (
|
||||
f"{instrument_id}.{exchange_id}"
|
||||
if instrument_id and exchange_id
|
||||
else instrument_id
|
||||
)
|
||||
remark = str(data.get("m_strRemark") or "")
|
||||
trade_date = str(data.get("m_strTradeDate") or "")
|
||||
trade_time = str(data.get("m_strTradeTime") or "")
|
||||
return cls(
|
||||
id=str(data.get("m_strTradeID") or ""),
|
||||
order_id=str(data.get("m_strOrderSysID") or ""),
|
||||
code=code,
|
||||
side={"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}.get(
|
||||
str(data.get("m_nOffsetFlag")), ""
|
||||
),
|
||||
remark=remark,
|
||||
traded_at=_parse_datetime(trade_date, trade_time),
|
||||
volume=_number(data.get("m_nVolume"), int),
|
||||
price=_number(data.get("m_dPrice")),
|
||||
amount=_number(data.get("m_dTradeAmount")),
|
||||
local_order_id=remark.split("|", 1)[0] if remark else "",
|
||||
order_ref=str(data.get("m_strOrderRef") or ""),
|
||||
exchange_id=exchange_id,
|
||||
name=str(data.get("m_strInstrumentName") or ""),
|
||||
account_id=str(data.get("m_strAccountID") or ""),
|
||||
commission=_number(
|
||||
data.get(
|
||||
"m_dCommission",
|
||||
data.get("m_dComission", data.get("m_dComssion")),
|
||||
)
|
||||
),
|
||||
trade_date=trade_date,
|
||||
trade_time=trade_time,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PositionItem:
|
||||
stock_code: str = ""
|
||||
stock_name: str = ""
|
||||
trade_id:str = ""
|
||||
trade_id: str = ""
|
||||
direction: Any = None
|
||||
volume: int = 0
|
||||
open_cost: float = 0.0
|
||||
@@ -88,15 +152,24 @@ class PositionItem:
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any], code: str = "") -> "PositionItem":
|
||||
return cls(
|
||||
stock_code=str(data.get("StockCode") or code), stock_name=str(data.get("StockName") or ""),
|
||||
trade_id=str(data.get("TradeID") or ""),open_cost=_number(data.get("OpenCost")),
|
||||
direction=data.get("Direction"), volume=_number(data.get("Volume"), int),
|
||||
open_price=_number(data.get("OpenPrice")), float_profit=_number(data.get("FloatProfit")),
|
||||
market_value=_number(data.get("MarketValue")), stock_holder=str(data.get("StockHolder") or ""),
|
||||
frozen_volume=_number(data.get("FrozenVolume"), int), can_use_volume=_number(data.get("CanUseVolume"), int),
|
||||
on_road_volume=_number(data.get("OnRoadVolume"), int), yesterday_volume=_number(data.get("YesterdayVolume"), int),
|
||||
last_price=_number(data.get("LastPrice")), profit_rate=_number(data.get("ProfitRate")),
|
||||
future_trade_type=data.get("FutureTradeType"), expire_date=str(data.get("ExpireDate") or ""),
|
||||
stock_code=str(data.get("StockCode") or code),
|
||||
stock_name=str(data.get("StockName") or ""),
|
||||
trade_id=str(data.get("TradeID") or ""),
|
||||
open_cost=_number(data.get("OpenCost")),
|
||||
direction=data.get("Direction"),
|
||||
volume=_number(data.get("Volume"), int),
|
||||
open_price=_number(data.get("OpenPrice")),
|
||||
float_profit=_number(data.get("FloatProfit")),
|
||||
market_value=_number(data.get("MarketValue")),
|
||||
stock_holder=str(data.get("StockHolder") or ""),
|
||||
frozen_volume=_number(data.get("FrozenVolume"), int),
|
||||
can_use_volume=_number(data.get("CanUseVolume"), int),
|
||||
on_road_volume=_number(data.get("OnRoadVolume"), int),
|
||||
yesterday_volume=_number(data.get("YesterdayVolume"), int),
|
||||
last_price=_number(data.get("LastPrice")),
|
||||
profit_rate=_number(data.get("ProfitRate")),
|
||||
future_trade_type=data.get("FutureTradeType"),
|
||||
expire_date=str(data.get("ExpireDate") or ""),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -146,8 +219,14 @@ class Portfolio:
|
||||
|
||||
|
||||
def _trade_datetime(data: dict[str, Any]) -> datetime | None:
|
||||
date = str(data.get("m_strInsertDate") or "")
|
||||
clock = str(data.get("m_strInsertTime") or "").replace(":", "").zfill(6)
|
||||
return _parse_datetime(
|
||||
str(data.get("m_strInsertDate") or ""),
|
||||
str(data.get("m_strInsertTime") or ""),
|
||||
)
|
||||
|
||||
|
||||
def _parse_datetime(date: str, clock: str) -> datetime | None:
|
||||
clock = clock.replace(":", "").zfill(6)
|
||||
try:
|
||||
return datetime.strptime(date + clock, "%Y%m%d%H%M%S")
|
||||
except ValueError:
|
||||
@@ -165,8 +244,12 @@ class Tick:
|
||||
if not isinstance(data, dict):
|
||||
return cls()
|
||||
return cls(
|
||||
last_price=_number(data.get("lastPrice", data.get("last_price", data.get("LastPrice")))),
|
||||
last_close=_number(data.get("lastClose", data.get("last_close", data.get("LastClose")))),
|
||||
last_price=_number(
|
||||
data.get("lastPrice", data.get("last_price", data.get("LastPrice")))
|
||||
),
|
||||
last_close=_number(
|
||||
data.get("lastClose", data.get("last_close", data.get("LastClose")))
|
||||
),
|
||||
raw=data,
|
||||
)
|
||||
|
||||
@@ -193,28 +276,54 @@ class MarketDataRequest:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FinancialDataRequest:
|
||||
tabname: str = ""; colname: str = ""; market: str = ""; code: str = ""
|
||||
report_type: str = ""; barpos: int = 0
|
||||
field_list: list[str] = field(default_factory=list); stock_list: list[str] = field(default_factory=list)
|
||||
start_date: str = ""; end_date: str = ""
|
||||
tabname: str = ""
|
||||
colname: str = ""
|
||||
market: str = ""
|
||||
code: str = ""
|
||||
report_type: str = ""
|
||||
barpos: int = 0
|
||||
field_list: list[str] = field(default_factory=list)
|
||||
stock_list: list[str] = field(default_factory=list)
|
||||
start_date: str = ""
|
||||
end_date: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FactorDataRequest:
|
||||
field_list: list[str] = field(default_factory=list); stock_list: list[str] = field(default_factory=list)
|
||||
stock_code: str = ""; start_date: str = ""; end_date: str = ""
|
||||
field_list: list[str] = field(default_factory=list)
|
||||
stock_list: list[str] = field(default_factory=list)
|
||||
stock_code: str = ""
|
||||
start_date: str = ""
|
||||
end_date: str = ""
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BSMPriceRequest:
|
||||
option_type: str; object_prices: Any; strike_price: float; risk_free: float; sigma: float; days: int; dividend: float
|
||||
option_type: str
|
||||
object_prices: Any
|
||||
strike_price: float
|
||||
risk_free: float
|
||||
sigma: float
|
||||
days: int
|
||||
dividend: float
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BSMIVRequest:
|
||||
option_type: str; object_prices: float; strike_price: float; option_price: float; risk_free: float; days: int; dividend: float
|
||||
option_type: str
|
||||
object_prices: float
|
||||
strike_price: float
|
||||
option_price: float
|
||||
risk_free: float
|
||||
days: int
|
||||
dividend: float
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class LocalDataRequest:
|
||||
stock_code: str; start_time: str = ""; end_time: str = ""; period: str = ""; divid_type: str = ""; count: int = 0
|
||||
stock_code: str
|
||||
start_time: str = ""
|
||||
end_time: str = ""
|
||||
period: str = ""
|
||||
divid_type: str = ""
|
||||
count: int = 0
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from .models import Assets, OrderItem, Portfolio, PositionItem
|
||||
from .models import Assets, DealItem, OrderItem, Portfolio, PositionItem
|
||||
|
||||
|
||||
class PortfolioMixin:
|
||||
@@ -41,9 +41,9 @@ class PortfolioMixin:
|
||||
data = self._get_json("/api/portfolio/order") or []
|
||||
return [OrderItem.from_trade_detail(row) for row in data]
|
||||
|
||||
def deals(self) -> list[dict[str, Any]]:
|
||||
data = self._get_json("/api/portfolio/deal") or {}
|
||||
return data.get("deals", [])
|
||||
def deals(self) -> list[DealItem]:
|
||||
"""查询原始 Deal 成交对象并转换为标准成交记录。"""
|
||||
return [DealItem.from_trade_detail(row) for row in self.org("deal")]
|
||||
|
||||
def trade_detail_data(self, datatype: str) -> Any:
|
||||
datatype = str(datatype).strip().lower()
|
||||
|
||||
@@ -54,9 +54,10 @@ def StartZT() -> None:
|
||||
# 收盘前最后一次只读对账,不发新单;未完成买回继续持久保存。
|
||||
try:
|
||||
portfolio = client.portfolio()
|
||||
deals = client.deals()
|
||||
state.reconcile(
|
||||
list(portfolio.positions.values()),
|
||||
portfolio.orders,
|
||||
deals,
|
||||
now.date().isoformat(),
|
||||
)
|
||||
except Exception:
|
||||
@@ -84,10 +85,11 @@ def RunOnce(run: Runtime, state: TState) -> None:
|
||||
|
||||
# 1. 一次获取资产、持仓和订单,并清理过期订单。
|
||||
portfolio = run.client.portfolio()
|
||||
deals = run.client.deals()
|
||||
positions = list(portfolio.positions.values())
|
||||
run.orders.refresh(run.client, portfolio.orders)
|
||||
# 对账使用完整原始订单列表,不能丢弃撤单和废单的部分成交。
|
||||
state.reconcile(positions, portfolio.orders, today)
|
||||
# 状态只按真实成交记账,不使用委托状态推算数量和成本。
|
||||
state.reconcile(positions, deals, today)
|
||||
|
||||
# 2. 获取本策略的信号开仓数据;信号失败不阻断已有做 T 买回。
|
||||
try:
|
||||
|
||||
@@ -9,10 +9,9 @@ from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from time import time
|
||||
|
||||
from sdk import OrderItem, PositionItem
|
||||
from sdk import DealItem, PositionItem
|
||||
|
||||
READY, SOLD, DONE = "READY", "SOLD", "DONE"
|
||||
TERMINAL_STATUSES = {"53", "54", "56", "57"}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -72,14 +71,14 @@ class TState:
|
||||
raise
|
||||
|
||||
def reconcile(
|
||||
self, positions: list[PositionItem], orders: list[OrderItem], today: str
|
||||
self, positions: list[PositionItem], deals: list[DealItem], today: str
|
||||
) -> None:
|
||||
"""先按实际成交记账,再接管未知持仓;不覆盖已记录的底仓成本。"""
|
||||
# 同一本地委托可能拆单;按券商订单号去重,数量齐全且全部结束才记账。
|
||||
by_id: dict[str, dict[str, OrderItem]] = {}
|
||||
for order in orders:
|
||||
if order.local_order_id:
|
||||
by_id.setdefault(order.local_order_id, {})[order.id] = order
|
||||
# 同一本地委托可能有多笔成交;按成交编号去重后合并数量和金额。
|
||||
by_id: dict[str, dict[str, DealItem]] = {}
|
||||
for deal in deals:
|
||||
if deal.local_order_id and deal.id:
|
||||
by_id.setdefault(deal.local_order_id, {})[deal.id] = deal
|
||||
for order_id, pending in list(self.pending.items()):
|
||||
side = "SELL" if pending.kind == "sell" else "BUY"
|
||||
rows = [
|
||||
@@ -88,25 +87,16 @@ class TState:
|
||||
if row.code == pending.code and row.side == side
|
||||
]
|
||||
if not rows:
|
||||
log.warning("[ZT 状态] 委托暂未查到,保留待确认:%s", order_id)
|
||||
log.warning("[ZT 状态] 成交暂未查到,保留待确认:%s", order_id)
|
||||
continue
|
||||
if sum(row.volume for row in rows) != pending.qty:
|
||||
qty = sum(row.volume for row in rows)
|
||||
# 成交未达到计划数量时继续等待,防止后续成交到达后重复记账。
|
||||
if qty != pending.qty:
|
||||
continue
|
||||
if any(row.status not in TERMINAL_STATUSES for row in rows):
|
||||
continue
|
||||
if any(
|
||||
row.status == "56" and row.traded_volume != row.volume for row in rows
|
||||
):
|
||||
continue
|
||||
qty = sum(row.traded_volume for row in rows)
|
||||
amounts = [
|
||||
(
|
||||
row.trade_amount
|
||||
if row.trade_amount > 0
|
||||
else row.trade_price * row.traded_volume
|
||||
)
|
||||
row.amount if row.amount > 0 else row.price * row.volume
|
||||
for row in rows
|
||||
if row.traded_volume > 0
|
||||
if row.volume > 0
|
||||
]
|
||||
if any(not math.isfinite(amount) or amount <= 0 for amount in amounts):
|
||||
continue
|
||||
@@ -132,7 +122,7 @@ class TState:
|
||||
item.phase = DONE if total >= item.sell_qty else SOLD
|
||||
if item.phase == DONE:
|
||||
item.trade_date = today
|
||||
# 零成交撤单也记录,保留计划、实际数量、均价和柜台终态。
|
||||
# 记录真实成交编号,重启后仍可核对本次状态变更的来源。
|
||||
self.records.append(
|
||||
{
|
||||
**asdict(pending),
|
||||
@@ -140,7 +130,7 @@ class TState:
|
||||
"filled_qty": qty,
|
||||
"filled_cost": cost,
|
||||
"amount": amount,
|
||||
"statuses": [row.status for row in rows],
|
||||
"deal_ids": [row.id for row in rows],
|
||||
}
|
||||
)
|
||||
del self.pending[order_id]
|
||||
@@ -201,19 +191,10 @@ class TState:
|
||||
if not self.path.is_file():
|
||||
return
|
||||
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
# 兼容原 ZT 文件,保留原底仓成本;没有额外版本字段。
|
||||
self.items = {
|
||||
code: TStateItem(**item) for code, item in raw.get("items", raw).items()
|
||||
code: TStateItem(**item) for code, item in raw["items"].items()
|
||||
}
|
||||
self.pending = {
|
||||
key: PendingOrder(**item) for key, item in raw.get("pending", {}).items()
|
||||
key: PendingOrder(**item) for key, item in raw["pending"].items()
|
||||
}
|
||||
self.records = raw.get("records", [])
|
||||
if "items" not in raw:
|
||||
# 旧记录只有提交行情价,不把它伪装成真实成交历史。
|
||||
self.records.append({"kind": "legacy_import", "items": raw})
|
||||
for item in self.items.values():
|
||||
if item.phase in {"SELLING", "BUYING", SOLD}:
|
||||
raise ValueError(
|
||||
f"[ZT 状态] {item.code} 旧做 T 轮次未结束,需先核对成交再迁移"
|
||||
)
|
||||
self.records = raw["records"]
|
||||
|
||||
Reference in New Issue
Block a user