feat: 持仓委托改止盈止损,保证金改读CTP柜台UseMargin

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 14:50:45 +08:00
parent 63beda3c71
commit 01de8dfb69
3 changed files with 99 additions and 11 deletions
+60 -1
View File
@@ -106,6 +106,8 @@ class CtpBridge:
self._commission_hooked = False
self._subscribed: set[str] = set()
self._last_position_query_ts: float = 0.0
self._position_margins: dict[str, float] = {}
self._margin_hooked = False
self._tick_hooked = False
self._bar_generators: dict[str, Any] = {}
self._bars_1m: dict[str, deque] = {}
@@ -223,7 +225,12 @@ class CtpBridge:
self._connected_mode = mode
self._last_error = ""
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
self._install_position_margin_hook()
self._schedule_fee_sync(mode)
try:
self.refresh_positions()
except Exception as exc:
logger.debug("initial position query: %s", exc)
return
time.sleep(0.5)
finally:
@@ -699,6 +706,52 @@ class CtpBridge:
"accountid": getattr(acc, "accountid", ""),
}
def _position_margin_key(self, sym: str, direction: str) -> str:
return f"{(sym or '').lower()}:{(direction or 'long').strip().lower()}"
def _install_position_margin_hook(self) -> None:
"""拦截 CTP 持仓回报,缓存柜台 UseMargin。"""
if self._margin_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, "onRspQryInvestorPosition"):
return
bridge = self
original = td.onRspQryInvestorPosition
def _wrapped(data, error, reqid, last):
try:
if data and isinstance(data, dict):
sym = (data.get("InstrumentID") or "").strip()
pos_dir = str(data.get("PosiDirection") or "")
if pos_dir == "2":
d = "long"
elif pos_dir == "3":
d = "short"
else:
d = "long" if "LONG" in pos_dir.upper() else "short"
margin = float(
data.get("UseMargin") or data.get("ExchangeMargin") or 0
)
if sym and margin > 0:
k = bridge._position_margin_key(sym, d)
bridge._position_margins[k] = (
bridge._position_margins.get(k, 0.0) + margin
)
except Exception as exc:
logger.debug("margin hook row: %s", exc)
return original(data, error, reqid, last)
td.onRspQryInvestorPosition = _wrapped
self._margin_hooked = True
except Exception as exc:
logger.debug("install margin hook: %s", exc)
def _lookup_position_margin(self, sym: str, direction: str) -> float:
return float(self._position_margins.get(self._position_margin_key(sym, direction), 0) or 0)
def _collect_positions(self) -> list[dict[str, Any]]:
if not self._engine:
return []
@@ -711,6 +764,7 @@ class CtpBridge:
sym = getattr(pos, "symbol", "") or ""
exchange = getattr(pos, "exchange", None)
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
margin = self._lookup_position_margin(sym, d)
out.append({
"symbol": sym,
"exchange": ex_name,
@@ -719,6 +773,7 @@ class CtpBridge:
"avg_price": float(getattr(pos, "price", 0) or 0),
"pnl": float(getattr(pos, "pnl", 0) or 0),
"frozen": int(getattr(pos, "frozen", 0) or 0),
"margin": round(margin, 2) if margin > 0 else None,
})
return out
@@ -731,15 +786,19 @@ class CtpBridge:
return
self._last_position_query_ts = now
try:
self._install_position_margin_hook()
gw = self._engine.get_gateway(GATEWAY_NAME)
td = getattr(gw, "td_api", None)
if td and hasattr(td, "query_position"):
self._position_margins.clear()
td.query_position()
time.sleep(0.4)
except Exception as exc:
logger.debug("refresh_positions: %s", exc)
def list_positions(self, *, refresh_if_empty: bool = True) -> list[dict[str, Any]]:
def list_positions(self, *, refresh_if_empty: bool = True, refresh_margin: bool = True) -> list[dict[str, Any]]:
if self._engine and self._connected_mode and refresh_margin:
self.refresh_positions()
out = self._collect_positions()
if not out and refresh_if_empty:
self.refresh_positions()