feat: 行情K线优先CTP tick聚合,修复手续费同步主力列表解析

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 13:18:43 +08:00
parent 09f4649d79
commit 3fe4add8e1
8 changed files with 390 additions and 24 deletions
+172 -2
View File
@@ -5,6 +5,7 @@ import logging
import os
import threading
import time
from collections import deque
from typing import Any, Optional
from locale_fix import ensure_process_locale
@@ -91,6 +92,8 @@ class CtpBridge:
self._commission_hooked = False
self._subscribed: set[str] = set()
self._tick_hooked = False
self._bar_generators: dict[str, Any] = {}
self._bars_1m: dict[str, deque] = {}
self._init_engine()
def _init_engine(self) -> None:
@@ -241,6 +244,12 @@ class CtpBridge:
bridge = self
def on_rsp(data: dict, error: dict, reqid: int, last: bool) -> None:
if error and int(error.get("ErrorID") or 0) != 0:
logger.debug(
"CTP commission error reqid=%s: %s",
reqid,
error.get("ErrorMsg") or error,
)
if data and data.get("InstrumentID"):
bridge._commission_results[reqid] = dict(data)
ev = bridge._commission_waiters.get(reqid)
@@ -255,8 +264,7 @@ class CtpBridge:
if self._connected_mode != mode or not self._engine:
return {}
try:
from ctp_symbol import ths_to_vnpy_symbol
sym, _ = ths_to_vnpy_symbol(ths_code)
sym, ex_name = ths_to_vnpy_symbol(ths_code)
gw = self._engine.get_gateway(GATEWAY_NAME)
td = gw.td_api
except Exception as exc:
@@ -275,6 +283,7 @@ class CtpBridge:
"BrokerID": td.brokerid,
"InvestorID": td.userid,
"InstrumentID": sym,
"ExchangeID": ex_name,
}
ret = td.reqQryInstrumentCommissionRate(req, reqid)
if ret != 0:
@@ -315,11 +324,160 @@ class CtpBridge:
logger.debug("lookup tick: %s", exc)
return None
def _bar_to_dict(self, bar: Any) -> dict:
dt = getattr(bar, "datetime", None)
d_str = dt.strftime("%Y-%m-%d %H:%M:%S") if dt else ""
return {
"d": d_str,
"o": float(getattr(bar, "open_price", 0) or 0),
"h": float(getattr(bar, "high_price", 0) or 0),
"l": float(getattr(bar, "low_price", 0) or 0),
"c": float(getattr(bar, "close_price", 0) or 0),
"v": float(getattr(bar, "volume", 0) or 0),
}
def _ensure_bar_generator(self, sym: str, ex_name: str) -> None:
key = self._tick_key(sym, ex_name)
if key in self._bar_generators:
return
self._bars_1m[key] = deque(maxlen=4000)
def on_bar(bar: Any) -> None:
row = self._bar_to_dict(bar)
if row.get("d"):
self._bars_1m[key].append(row)
try:
from vnpy.trader.utility import BarGenerator
self._bar_generators[key] = BarGenerator(on_bar=on_bar)
except ImportError:
logger.debug("BarGenerator unavailable")
def _find_tick(self, symbol: str, ex_name: str) -> Any:
if not self._engine:
return None
sym_l = symbol.lower()
ex_u = ex_name.upper()
try:
for tick in self._engine.get_all_ticks():
ts = (getattr(tick, "symbol", "") or "").lower()
te = getattr(tick, "exchange", None)
te_s = str(te.value if hasattr(te, "value") else te or "").upper()
if ts == sym_l and te_s == ex_u:
return tick
except Exception as exc:
logger.debug("find tick: %s", exc)
return None
def _tick_to_bar(self, symbol: str, ex_name: str) -> Optional[dict]:
tick = self._find_tick(symbol, ex_name)
if not tick:
return None
lp = self._price_from_tick(tick)
if not lp or lp <= 0:
return None
dt = getattr(tick, "datetime", None)
d_str = dt.strftime("%Y-%m-%d %H:%M:%S") if dt else ""
if not d_str:
from datetime import datetime
from zoneinfo import ZoneInfo
d_str = datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y-%m-%d %H:%M:%S")
o = float(getattr(tick, "open_price", 0) or lp)
h = float(getattr(tick, "high_price", 0) or lp)
lo = float(getattr(tick, "low_price", 0) or lp)
return {
"d": d_str,
"o": o,
"h": h,
"l": lo,
"c": lp,
"v": float(getattr(tick, "volume", 0) or 0),
}
def _on_tick(self, tick: Any) -> None:
sym = (getattr(tick, "symbol", "") or "").lower()
te = getattr(tick, "exchange", None)
ex_s = str(te.value if hasattr(te, "value") else te or "").upper()
key = self._tick_key(sym, ex_s)
bg = self._bar_generators.get(key)
if not bg:
return
try:
bg.update_tick(tick)
except Exception as exc:
logger.debug("bar gen tick: %s", exc)
def _ensure_tick_handler(self) -> None:
if self._tick_hooked or not self._ee:
return
try:
from vnpy.trader.event import EVENT_TICK
except ImportError:
return
def process_tick(event: Any) -> None:
self._on_tick(event.data)
self._ee.register(EVENT_TICK, process_tick)
self._tick_hooked = True
def get_kline_bars_1m(self, ths_code: str, *, mode: str) -> list[dict]:
"""订阅合约并返回 1 分钟 K 线(含正在形成的 bar)。"""
if self._connected_mode != mode or not self._engine:
return []
try:
sym, ex_name = ths_to_vnpy_symbol(ths_code)
except Exception:
return []
key = self._tick_key(sym, ex_name)
self._ensure_bar_generator(sym, ex_name)
self.subscribe_symbol(ths_code)
for _ in range(12):
if self._bars_1m.get(key) and len(self._bars_1m[key]) > 0:
break
if self._lookup_tick(sym, ex_name):
break
time.sleep(0.2)
bars_1m = list(self._bars_1m.get(key, []))
bg = self._bar_generators.get(key)
if bg and getattr(bg, "bar", None):
forming = self._bar_to_dict(bg.bar)
if forming.get("d"):
if not bars_1m or bars_1m[-1]["d"] != forming["d"]:
bars_1m.append(forming)
else:
bars_1m[-1] = forming
if not bars_1m:
tick_bar = self._tick_to_bar(sym, ex_name)
if tick_bar:
bars_1m = [tick_bar]
return bars_1m
def get_tick_detail(self, ths_code: str, *, mode: str) -> dict[str, Any]:
if self._connected_mode != mode or not self._engine:
return {}
try:
sym, ex_name = ths_to_vnpy_symbol(ths_code)
except Exception:
return {}
self.subscribe_symbol(ths_code)
for _ in range(8):
tick = self._find_tick(sym, ex_name)
if tick:
price = self._price_from_tick(tick)
try:
pre_close = float(getattr(tick, "pre_close", 0) or 0)
except (TypeError, ValueError):
pre_close = 0.0
return {
"price": price,
"pre_close": pre_close if pre_close > 0 else None,
}
time.sleep(0.2)
return {}
def subscribe_symbol(self, ths_code: str) -> None:
if not self._engine or not self._connected_mode:
return
@@ -328,6 +486,7 @@ class CtpBridge:
sym, ex_name = ths_to_vnpy_symbol(ths_code)
key = self._tick_key(sym, ex_name)
self._ensure_bar_generator(sym, ex_name)
if key in self._subscribed:
return
exchange = to_vnpy_exchange(ex_name)
@@ -564,6 +723,17 @@ def ctp_get_tick_price(mode: str, ths_code: str) -> Optional[float]:
return None
def ctp_get_tick_detail(mode: str, ths_code: str) -> dict[str, Any]:
b = get_bridge()
if b.connected_mode != mode:
return {}
try:
return b.get_tick_detail(ths_code, mode=mode)
except Exception as exc:
logger.debug("ctp_get_tick_detail: %s", exc)
return {}
def get_ctp_balance(mode: str) -> Optional[float]:
try:
acc = ctp_get_account(mode)