feat: 导航开关与 CTP 柜台手续费

系统设置可开关五类导航;手续费默认从 CTP 查询同步,本地/AKShare 作离线兜底;补充 FEES.md。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 12:19:56 +08:00
parent ca894dfd4d
commit 528d9811e3
13 changed files with 523 additions and 48 deletions
+69
View File
@@ -86,6 +86,9 @@ class CtpBridge:
self._connected_mode: Optional[str] = None
self._last_error: str = ""
self._connect_lock = threading.Lock()
self._commission_waiters: dict[int, threading.Event] = {}
self._commission_results: dict[int, dict] = {}
self._commission_hooked = False
self._init_engine()
def _init_engine(self) -> None:
@@ -184,6 +187,7 @@ class CtpBridge:
self._connected_mode = mode
self._last_error = ""
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
self._schedule_fee_sync(mode)
return
time.sleep(0.5)
finally:
@@ -213,6 +217,71 @@ class CtpBridge:
def mark_disconnected(self) -> None:
self._connected_mode = None
def _schedule_fee_sync(self, mode: str) -> None:
def _run() -> None:
try:
from ctp_fee_sync import sync_fees_from_ctp
n, msg = sync_fees_from_ctp(mode, max_symbols=60)
logger.info("CTP 手续费同步: %s", msg if n else msg)
except Exception as exc:
logger.debug("CTP 手续费后台同步: %s", exc)
threading.Thread(target=_run, daemon=True, name="ctp-fee-sync").start()
def _ensure_commission_callback(self) -> None:
if self._commission_hooked or not self._engine:
return
try:
gw = self._engine.get_gateway(GATEWAY_NAME)
td = gw.td_api
except Exception:
return
bridge = self
def on_rsp(data: dict, error: dict, reqid: int, last: bool) -> None:
if data and data.get("InstrumentID"):
bridge._commission_results[reqid] = dict(data)
ev = bridge._commission_waiters.get(reqid)
if last and ev:
ev.set()
td.onRspQryInstrumentCommissionRate = on_rsp # type: ignore[method-assign]
self._commission_hooked = True
def query_instrument_commission(self, ths_code: str, *, mode: str) -> dict:
"""查询单合约 CTP 手续费率(需已连接)。"""
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)
gw = self._engine.get_gateway(GATEWAY_NAME)
td = gw.td_api
except Exception as exc:
logger.debug("commission query init: %s", exc)
return {}
if not getattr(td, "login_status", False):
return {}
if not hasattr(td, "reqQryInstrumentCommissionRate"):
return {}
self._ensure_commission_callback()
reqid = int(getattr(td, "reqid", 0)) + 1
td.reqid = reqid
ev = threading.Event()
self._commission_waiters[reqid] = ev
req = {
"BrokerID": td.brokerid,
"InvestorID": td.userid,
"InstrumentID": sym,
}
ret = td.reqQryInstrumentCommissionRate(req, reqid)
if ret != 0:
self._commission_waiters.pop(reqid, None)
return {}
ev.wait(timeout=8)
self._commission_waiters.pop(reqid, None)
return self._commission_results.pop(reqid, {})
def get_account(self) -> dict[str, Any]:
if not self._engine:
return {}