feat: 持仓委托改止盈止损,保证金改读CTP柜台UseMargin
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+60
-1
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user