Gate order cancel to trading hours and sync trade logs from CTP.

Disable cancel UI outside sessions, query exchange fills for records, and label local vs counterparty rows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 00:35:51 +08:00
parent a23f2c80ca
commit 9f48f22d16
9 changed files with 606 additions and 8 deletions
+193
View File
@@ -189,6 +189,10 @@ class CtpBridge:
self._position_margins: dict[str, float] = {}
self._position_open_times: dict[str, str] = {}
self._margin_hooked = False
self._trade_hooked = False
self._trade_query_results: list[dict[str, Any]] = []
self._trade_query_event = threading.Event()
self._last_trade_query_ts: float = 0.0
self._tick_hooked = False
self._bar_generators: dict[str, Any] = {}
self._bars_1m: dict[str, deque] = {}
@@ -1055,6 +1059,188 @@ class CtpBridge:
out = self._collect_positions()
return out
@staticmethod
def _parse_trade_offset(offset_obj: Any) -> str:
s = str(offset_obj or "").upper()
if "OPEN" in s:
return "open"
return "close"
@staticmethod
def _parse_trade_direction(direction_obj: Any) -> str:
return "long" if _is_long_direction(direction_obj) else "short"
@staticmethod
def _position_direction_from_trade(trade_direction: str, offset: str) -> str:
td = (trade_direction or "long").strip().lower()
if (offset or "open").strip().lower() == "open":
return td
return "short" if td == "long" else "long"
def _format_trade_datetime(self, dt_obj: Any, date_raw: str = "", time_raw: str = "") -> str:
if dt_obj is not None:
try:
if hasattr(dt_obj, "strftime"):
return dt_obj.strftime("%Y-%m-%d %H:%M:%S")
text = str(dt_obj).strip()
if text:
return text[:19].replace("T", " ")
except Exception:
pass
parsed = self._parse_ctp_open_datetime(date_raw, time_raw)
return parsed or ""
def _trade_row_from_vnpy(self, trade: Any) -> Optional[dict[str, Any]]:
try:
sym = (getattr(trade, "symbol", "") or "").strip()
vol = int(getattr(trade, "volume", 0) or 0)
if not sym or vol <= 0:
return None
direction = self._parse_trade_direction(getattr(trade, "direction", None))
offset = self._parse_trade_offset(getattr(trade, "offset", None))
exchange = getattr(trade, "exchange", None)
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
dt = self._format_trade_datetime(getattr(trade, "datetime", None))
trade_id = str(getattr(trade, "tradeid", "") or getattr(trade, "vt_tradeid", "") or "")
order_id = str(getattr(trade, "orderid", "") or getattr(trade, "vt_orderid", "") or "")
if not trade_id:
trade_id = f"{order_id}:{sym}:{offset}:{direction}:{vol}:{getattr(trade, 'price', 0)}:{dt}"
return {
"trade_id": trade_id,
"order_id": order_id,
"symbol": sym,
"exchange": ex_name,
"direction": direction,
"offset": offset,
"position_direction": self._position_direction_from_trade(direction, offset),
"lots": vol,
"price": float(getattr(trade, "price", 0) or 0),
"datetime": dt,
}
except Exception as exc:
logger.debug("trade_row_from_vnpy: %s", exc)
return None
def _trade_row_from_ctp_dict(self, data: dict) -> Optional[dict[str, Any]]:
try:
sym = (data.get("InstrumentID") or data.get("instrument_id") or "").strip()
vol = int(float(data.get("Volume") or data.get("volume") or 0))
if not sym or vol <= 0:
return None
dir_raw = str(data.get("Direction") or data.get("direction") or "")
direction = "long" if dir_raw in ("0", "2") or "LONG" in dir_raw.upper() or dir_raw == "" else "short"
off_raw = str(data.get("OffsetFlag") or data.get("offset") or "")
if off_raw in ("0",) or "OPEN" in off_raw.upper():
offset = "open"
else:
offset = "close"
price = float(data.get("Price") or data.get("price") or 0)
trade_id = str(data.get("TradeID") or data.get("tradeid") or "").strip()
order_sys = str(data.get("OrderSysID") or data.get("orderid") or "").strip()
dt = self._format_trade_datetime(
None,
str(data.get("TradeDate") or data.get("trade_date") or ""),
str(data.get("TradeTime") or data.get("trade_time") or ""),
)
if not trade_id:
trade_id = f"{order_sys}:{sym}:{offset}:{direction}:{vol}:{price}:{dt}"
return {
"trade_id": trade_id,
"order_id": order_sys,
"symbol": sym,
"exchange": str(data.get("ExchangeID") or data.get("exchange") or ""),
"direction": direction,
"offset": offset,
"position_direction": self._position_direction_from_trade(direction, offset),
"lots": vol,
"price": price,
"datetime": dt,
}
except Exception as exc:
logger.debug("trade_row_from_ctp_dict: %s", exc)
return None
def _install_trade_query_hook(self) -> None:
if self._trade_hooked or not self._engine:
return
try:
gw = self._engine.get_gateway(GATEWAY_NAME)
td = getattr(gw, "td_api", None)
if not td or not hasattr(td, "onRspQryTrade"):
return
bridge = self
original = td.onRspQryTrade
def _wrapped(data, error, reqid, last):
try:
if data and isinstance(data, dict):
row = bridge._trade_row_from_ctp_dict(data)
if row:
bridge._trade_query_results.append(row)
except Exception as exc:
logger.debug("trade hook row: %s", exc)
result = original(data, error, reqid, last)
if last:
bridge._trade_query_event.set()
return result
td.onRspQryTrade = _wrapped
self._trade_hooked = True
except Exception as exc:
logger.debug("install trade hook: %s", exc)
def _collect_engine_trades(self) -> list[dict[str, Any]]:
if not self._engine:
return []
out: list[dict[str, Any]] = []
seen: set[str] = set()
try:
trades = self._engine.get_all_trades()
except Exception:
trades = {}
for trade in (trades or {}).values():
row = self._trade_row_from_vnpy(trade)
if not row:
continue
key = row["trade_id"]
if key in seen:
continue
seen.add(key)
out.append(row)
return out
def refresh_trades(self) -> None:
"""向柜台查询当日成交(并合并内存成交回报)。"""
if not self._engine:
return
now = time.time()
if now - self._last_trade_query_ts < 1.0:
return
self._last_trade_query_ts = now
self._trade_query_results = []
self._trade_query_event.clear()
try:
self._install_trade_query_hook()
gw = self._engine.get_gateway(GATEWAY_NAME)
td = getattr(gw, "td_api", None)
if td and hasattr(td, "query_trade"):
td.query_trade()
self._trade_query_event.wait(timeout=2.0)
except Exception as exc:
logger.debug("refresh_trades: %s", exc)
def list_trades(self, *, refresh: bool = False) -> list[dict[str, Any]]:
if refresh:
self.refresh_trades()
merged: dict[str, dict[str, Any]] = {}
for row in self._collect_engine_trades():
merged[row["trade_id"]] = row
for row in self._trade_query_results:
merged[row["trade_id"]] = row
out = list(merged.values())
out.sort(key=lambda r: (r.get("datetime") or "", r.get("trade_id") or ""))
return out
def list_active_orders(self) -> list[dict[str, Any]]:
if not self._engine:
return []
@@ -1282,6 +1468,13 @@ def ctp_cancel_order(mode: str, vt_orderid: str) -> bool:
return b.cancel_order(vt_orderid)
def ctp_list_trades(mode: str, *, refresh: bool = False) -> list[dict[str, Any]]:
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return []
return b.list_trades(refresh=refresh)
def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
"""CTP 柜台最新价(需已连接并订阅)。"""
b = get_bridge()