feat: 行情K线优先CTP tick聚合,修复手续费同步主力列表解析
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+172
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user