Use CTP contract margin rates for position margin and ratio display.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+63
-14
@@ -88,6 +88,7 @@ from vnpy_bridge import (
|
|||||||
_ctp_td_lock,
|
_ctp_td_lock,
|
||||||
ctp_cancel_order,
|
ctp_cancel_order,
|
||||||
ctp_connect,
|
ctp_connect,
|
||||||
|
ctp_estimate_margin_one_lot,
|
||||||
ctp_get_account,
|
ctp_get_account,
|
||||||
ctp_get_tick_price,
|
ctp_get_tick_price,
|
||||||
ctp_list_active_orders,
|
ctp_list_active_orders,
|
||||||
@@ -211,6 +212,41 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
return codes.get("ths_code") or sym
|
return codes.get("ths_code") or sym
|
||||||
return sym
|
return sym
|
||||||
|
|
||||||
|
def _resolve_position_margin(
|
||||||
|
*,
|
||||||
|
sym: str,
|
||||||
|
direction: str,
|
||||||
|
lots: int,
|
||||||
|
entry: float,
|
||||||
|
mode: str,
|
||||||
|
ctp: Optional[dict] = None,
|
||||||
|
mon_margin: Optional[float] = None,
|
||||||
|
est_margin: Optional[float] = None,
|
||||||
|
) -> tuple[Optional[float], str]:
|
||||||
|
"""占用保证金:柜台持仓 > CTP 合约率估算 > 本地规格估算 > 库内缓存。"""
|
||||||
|
ctp_margin = float(ctp.get("margin") or 0) if ctp else 0.0
|
||||||
|
if ctp_margin > 0:
|
||||||
|
return round(ctp_margin, 2), "ctp"
|
||||||
|
connected = bool(ctp_status(mode).get("connected"))
|
||||||
|
ths_sym = sym
|
||||||
|
if ctp:
|
||||||
|
ths_sym = _ctp_pos_to_ths_code(ctp) or sym
|
||||||
|
else:
|
||||||
|
codes = ths_to_codes(sym)
|
||||||
|
if codes and codes.get("ths_code"):
|
||||||
|
ths_sym = codes["ths_code"]
|
||||||
|
if connected and ths_sym and entry > 0 and lots > 0:
|
||||||
|
per_lot = ctp_estimate_margin_one_lot(
|
||||||
|
mode, ths_sym, entry, direction=direction,
|
||||||
|
)
|
||||||
|
if per_lot and per_lot > 0:
|
||||||
|
return round(per_lot * lots, 2), "ctp"
|
||||||
|
if est_margin and float(est_margin) > 0:
|
||||||
|
return round(float(est_margin), 2), "estimate"
|
||||||
|
if not connected and mon_margin and float(mon_margin) > 0:
|
||||||
|
return round(float(mon_margin), 2), "db"
|
||||||
|
return None, "estimate"
|
||||||
|
|
||||||
def _ensure_monitors_from_ctp(conn, mode: str) -> None:
|
def _ensure_monitors_from_ctp(conn, mode: str) -> None:
|
||||||
"""CTP 有持仓但本地无监控时,自动补写一条 active 记录供展示。"""
|
"""CTP 有持仓但本地无监控时,自动补写一条 active 记录供展示。"""
|
||||||
if not ctp_status(mode).get("connected"):
|
if not ctp_status(mode).get("connected"):
|
||||||
@@ -611,7 +647,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
mark = ctp_get_tick_price(mode, sym)
|
mark = ctp_get_tick_price(mode, sym)
|
||||||
if mark is None or mark <= 0:
|
if mark is None or mark <= 0:
|
||||||
mark = entry if entry else None
|
mark = entry if entry else None
|
||||||
margin = ctp_margin if ctp_margin > 0 else None
|
est = calc_position_metrics(
|
||||||
|
direction, entry, entry, entry, lots, mark or entry, capital, sym,
|
||||||
|
).get("margin")
|
||||||
|
margin, _src = _resolve_position_margin(
|
||||||
|
sym=sym,
|
||||||
|
direction=direction,
|
||||||
|
lots=lots,
|
||||||
|
entry=entry,
|
||||||
|
mode=mode,
|
||||||
|
ctp=p,
|
||||||
|
est_margin=est,
|
||||||
|
)
|
||||||
position_pct = None
|
position_pct = None
|
||||||
if margin and capital > 0:
|
if margin and capital > 0:
|
||||||
position_pct = round(float(margin) / float(capital) * 100, 2)
|
position_pct = round(float(margin) / float(capital) * 100, 2)
|
||||||
@@ -733,20 +780,22 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
direction, entry, sl if sl is not None else entry,
|
direction, entry, sl if sl is not None else entry,
|
||||||
tp if tp is not None else entry, lots, mark, capital, sym,
|
tp if tp is not None else entry, lots, mark, capital, sym,
|
||||||
)
|
)
|
||||||
if margin is None or float(margin or 0) <= 0:
|
mon_margin = margin
|
||||||
ctp_margin = float(ctp.get("margin") or 0) if ctp else 0.0
|
margin, margin_source = _resolve_position_margin(
|
||||||
est_margin = pos_metrics.get("margin")
|
sym=sym,
|
||||||
margin = ctp_margin if ctp_margin > 0 else est_margin
|
direction=direction,
|
||||||
margin_source = "ctp" if ctp_margin > 0 else "estimate"
|
lots=lots,
|
||||||
else:
|
entry=entry,
|
||||||
margin_source = "ctp"
|
mode=mode,
|
||||||
if position_pct is None or float(position_pct or 0) <= 0:
|
ctp=ctp,
|
||||||
position_pct = (
|
mon_margin=mon_margin if isinstance(mon_margin, (int, float)) else None,
|
||||||
round(float(margin) / capital * 100, 2)
|
est_margin=pos_metrics.get("margin"),
|
||||||
if capital > 0 and margin
|
|
||||||
else pos_metrics.get("position_pct")
|
|
||||||
)
|
)
|
||||||
else:
|
if margin and capital > 0:
|
||||||
|
position_pct = round(float(margin) / float(capital) * 100, 2)
|
||||||
|
elif position_pct is None or float(position_pct or 0) <= 0:
|
||||||
|
position_pct = pos_metrics.get("position_pct")
|
||||||
|
elif position_pct is not None:
|
||||||
position_pct = float(position_pct)
|
position_pct = float(position_pct)
|
||||||
order_st = monitor_order_status(
|
order_st = monitor_order_status(
|
||||||
mon or {}, mode=mode, ths_code=sym, direction=direction,
|
mon or {}, mode=mode, ths_code=sym, direction=direction,
|
||||||
|
|||||||
+71
-6
@@ -976,7 +976,29 @@ class CtpBridge:
|
|||||||
def _lookup_position_margin(self, sym: str, direction: str) -> float:
|
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)
|
return float(self._position_margins.get(self._position_margin_key(sym, direction), 0) or 0)
|
||||||
|
|
||||||
def estimate_margin_one_lot(self, ths_code: str, price: float) -> Optional[float]:
|
@staticmethod
|
||||||
|
def _vnpy_sym_to_ths(sym: str, ex_name: str) -> str:
|
||||||
|
import re
|
||||||
|
|
||||||
|
s = (sym or "").strip()
|
||||||
|
if not s:
|
||||||
|
return ""
|
||||||
|
ex = (ex_name or "").upper()
|
||||||
|
m = re.match(r"^([A-Za-z]+)(\d+)$", s)
|
||||||
|
if not m:
|
||||||
|
return s
|
||||||
|
letters, digits = m.group(1), m.group(2)
|
||||||
|
if ex == "CZCE":
|
||||||
|
return letters.upper() + (digits[-3:] if len(digits) >= 4 else digits)
|
||||||
|
return letters.lower() + digits
|
||||||
|
|
||||||
|
def estimate_margin_one_lot(
|
||||||
|
self,
|
||||||
|
ths_code: str,
|
||||||
|
price: float,
|
||||||
|
*,
|
||||||
|
direction: str = "long",
|
||||||
|
) -> Optional[float]:
|
||||||
"""用 CTP 合约信息估算 1 手保证金(需已连接并完成合约查询)。"""
|
"""用 CTP 合约信息估算 1 手保证金(需已连接并完成合约查询)。"""
|
||||||
if not self._engine or not price or price <= 0:
|
if not self._engine or not price or price <= 0:
|
||||||
return None
|
return None
|
||||||
@@ -990,6 +1012,12 @@ class CtpBridge:
|
|||||||
mult = float(getattr(contract, "size", 0) or 0)
|
mult = float(getattr(contract, "size", 0) or 0)
|
||||||
long_r = float(getattr(contract, "long_margin_ratio", 0) or 0)
|
long_r = float(getattr(contract, "long_margin_ratio", 0) or 0)
|
||||||
short_r = float(getattr(contract, "short_margin_ratio", 0) or 0)
|
short_r = float(getattr(contract, "short_margin_ratio", 0) or 0)
|
||||||
|
d = (direction or "long").strip().lower()
|
||||||
|
if d == "short" and short_r > 0:
|
||||||
|
ratio = short_r
|
||||||
|
elif d != "short" and long_r > 0:
|
||||||
|
ratio = long_r
|
||||||
|
else:
|
||||||
ratio = max(long_r, short_r)
|
ratio = max(long_r, short_r)
|
||||||
if mult <= 0 or ratio <= 0:
|
if mult <= 0 or ratio <= 0:
|
||||||
return None
|
return None
|
||||||
@@ -998,6 +1026,34 @@ class CtpBridge:
|
|||||||
logger.debug("estimate_margin_one_lot %s: %s", ths_code, exc)
|
logger.debug("estimate_margin_one_lot %s: %s", ths_code, exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def estimate_position_margin(
|
||||||
|
self,
|
||||||
|
sym: str,
|
||||||
|
ex_name: str,
|
||||||
|
direction: str,
|
||||||
|
lots: int,
|
||||||
|
price: float,
|
||||||
|
*,
|
||||||
|
pos: Any = None,
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""持仓占用保证金:优先 vnpy 字段,其次 CTP 合约保证金率估算。"""
|
||||||
|
if lots <= 0 or price <= 0:
|
||||||
|
return None
|
||||||
|
if pos is not None:
|
||||||
|
raw = float(getattr(pos, "margin", 0) or getattr(pos, "use_margin", 0) or 0)
|
||||||
|
if raw > 0:
|
||||||
|
return round(raw, 2)
|
||||||
|
cached = self._lookup_position_margin(sym, direction)
|
||||||
|
if cached > 0:
|
||||||
|
return round(cached, 2)
|
||||||
|
ths = self._vnpy_sym_to_ths(sym, ex_name)
|
||||||
|
if not ths:
|
||||||
|
return None
|
||||||
|
per_lot = self.estimate_margin_one_lot(ths, price, direction=direction)
|
||||||
|
if per_lot and per_lot > 0:
|
||||||
|
return round(per_lot * lots, 2)
|
||||||
|
return None
|
||||||
|
|
||||||
def lookup_contract_spec(self, ths_code: str) -> Optional[dict]:
|
def lookup_contract_spec(self, ths_code: str) -> Optional[dict]:
|
||||||
"""从 CTP 合约信息读取乘数与最小变动价位。"""
|
"""从 CTP 合约信息读取乘数与最小变动价位。"""
|
||||||
if not self._engine:
|
if not self._engine:
|
||||||
@@ -1037,17 +1093,20 @@ class CtpBridge:
|
|||||||
sym = getattr(pos, "symbol", "") or ""
|
sym = getattr(pos, "symbol", "") or ""
|
||||||
exchange = getattr(pos, "exchange", None)
|
exchange = getattr(pos, "exchange", None)
|
||||||
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
||||||
margin = self._lookup_position_margin(sym, d)
|
price = float(getattr(pos, "price", 0) or 0)
|
||||||
|
margin = self.estimate_position_margin(
|
||||||
|
sym, ex_name, d, vol, price, pos=pos,
|
||||||
|
)
|
||||||
open_time = self._lookup_position_open_time(sym, d) or None
|
open_time = self._lookup_position_open_time(sym, d) or None
|
||||||
out.append({
|
out.append({
|
||||||
"symbol": sym,
|
"symbol": sym,
|
||||||
"exchange": ex_name,
|
"exchange": ex_name,
|
||||||
"direction": d,
|
"direction": d,
|
||||||
"lots": vol,
|
"lots": vol,
|
||||||
"avg_price": float(getattr(pos, "price", 0) or 0),
|
"avg_price": price,
|
||||||
"pnl": float(getattr(pos, "pnl", 0) or 0),
|
"pnl": float(getattr(pos, "pnl", 0) or 0),
|
||||||
"frozen": int(getattr(pos, "frozen", 0) or 0),
|
"frozen": int(getattr(pos, "frozen", 0) or 0),
|
||||||
"margin": round(margin, 2) if margin > 0 else None,
|
"margin": margin,
|
||||||
"open_time": open_time,
|
"open_time": open_time,
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
@@ -1471,12 +1530,18 @@ def ctp_get_tick_detail(mode: str, ths_code: str) -> dict[str, Any]:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def ctp_estimate_margin_one_lot(mode: str, ths_code: str, price: float) -> Optional[float]:
|
def ctp_estimate_margin_one_lot(
|
||||||
|
mode: str,
|
||||||
|
ths_code: str,
|
||||||
|
price: float,
|
||||||
|
*,
|
||||||
|
direction: str = "long",
|
||||||
|
) -> Optional[float]:
|
||||||
b = get_bridge()
|
b = get_bridge()
|
||||||
if b.connected_mode != mode or not b.ping():
|
if b.connected_mode != mode or not b.ping():
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
return b.estimate_margin_one_lot(ths_code, price)
|
return b.estimate_margin_one_lot(ths_code, price, direction=direction)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("ctp_estimate_margin_one_lot: %s", exc)
|
logger.debug("ctp_estimate_margin_one_lot: %s", exc)
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user