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:
+193
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user