Align position margin with account balance and show deducted open commission only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 11:26:41 +08:00
parent 038eb9a403
commit 9a55c61678
3 changed files with 130 additions and 6 deletions
+92 -4
View File
@@ -88,11 +88,13 @@ from vnpy_bridge import (
_ctp_td_lock, _ctp_td_lock,
ctp_cancel_order, ctp_cancel_order,
ctp_connect, ctp_connect,
ctp_account_margin_used,
ctp_estimate_margin_one_lot, 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,
ctp_list_positions, ctp_list_positions,
ctp_list_trades,
ctp_status, ctp_status,
execute_order, execute_order,
get_bridge, get_bridge,
@@ -247,6 +249,56 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
return round(float(mon_margin), 2), "db" return round(float(mon_margin), 2), "db"
return None, "estimate" return None, "estimate"
def _apply_account_margin_to_rows(
rows: list[dict],
mode: str,
capital: float,
) -> list[dict]:
"""CTP 已连接时,用「权益−可用」校正占用保证金与仓位占比。"""
if not ctp_status(mode).get("connected"):
return rows
total_used = ctp_account_margin_used(mode)
if not total_used:
return rows
active = [
r for r in rows
if r.get("order_state") != "pending" and int(r.get("lots") or 0) > 0
]
if not active:
return rows
if len(active) == 1:
row = active[0]
row["margin"] = total_used
row["margin_source"] = "ctp"
if capital > 0:
row["position_pct"] = round(total_used / capital * 100, 2)
return rows
weights: list[float] = []
for row in active:
sym = (row.get("symbol_code") or "").strip()
lots = int(row.get("lots") or 0)
entry = float(row.get("entry_price") or 0)
if sym and lots > 0 and entry > 0:
spec = get_contract_spec(sym)
weights.append(entry * spec["mult"] * lots)
else:
weights.append(0.0)
total_weight = sum(weights)
assigned = 0.0
for i, row in enumerate(active):
if total_weight <= 0:
margin = round(total_used / len(active), 2)
elif i == len(active) - 1:
margin = round(total_used - assigned, 2)
else:
margin = round(total_used * weights[i] / total_weight, 2)
assigned += margin
row["margin"] = margin
row["margin_source"] = "ctp"
if capital > 0:
row["position_pct"] = round(margin / capital * 100, 2)
return rows
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"):
@@ -307,6 +359,34 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
pass pass
return False return False
def _open_commission_from_ctp_trades(
mode: str, sym: str, direction: str,
) -> Optional[float]:
"""汇总该持仓开仓成交的柜台手续费(成交回报中的 commission)。"""
if not ctp_status(mode).get("connected"):
return None
try:
trades = ctp_list_trades(mode)
except Exception:
return None
total = 0.0
has_commission = False
for t in trades:
if (t.get("offset") or "").strip().lower() != "open":
continue
pos_dir = (
t.get("position_direction") or t.get("direction") or "long"
).strip().lower()
if pos_dir != (direction or "long").strip().lower():
continue
if not _match_ctp_symbol(t.get("symbol") or "", sym):
continue
comm = float(t.get("commission") or 0)
total += comm
if comm > 0:
has_commission = True
return round(total, 2) if has_commission else None
def _holding_duration(open_time: str, now_iso: str) -> str: def _holding_duration(open_time: str, now_iso: str) -> str:
try: try:
from app import calc_holding_duration from app import calc_holding_duration
@@ -773,9 +853,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
fee_info = calc_fee_breakdown( fee_info = calc_fee_breakdown(
sym, entry, close_est, lots, open_time or now_iso, now_iso, trading_mode=mode, sym, entry, close_est, lots, open_time or now_iso, now_iso, trading_mode=mode,
) )
open_commission = _open_commission_from_ctp_trades(mode, sym, direction)
if open_commission is not None:
display_fee = open_commission
fee_source = "ctp"
else:
display_fee = fee_info["open_fee"]
fee_source = fee_info.get("fee_source") or "local"
est_net = None est_net = None
if float_pnl is not None: if float_pnl is not None:
est_net = round(float(float_pnl) - fee_info["total_fee"], 2) est_net = round(float(float_pnl) - fee_info["close_fee"], 2)
pos_metrics = calc_position_metrics( pos_metrics = calc_position_metrics(
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,
@@ -853,11 +940,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"risk_pct": pos_metrics.get("risk_pct") if sl is not None else None, "risk_pct": pos_metrics.get("risk_pct") if sl is not None else None,
"rr_ratio": pos_metrics.get("rr_ratio") if sl is not None and tp is not None else None, "rr_ratio": pos_metrics.get("rr_ratio") if sl is not None and tp is not None else None,
"float_pnl": float_pnl, "float_pnl": float_pnl,
"est_fee": fee_info["total_fee"], "est_fee": display_fee,
"est_fee_open": fee_info["open_fee"], "est_fee_open": display_fee,
"est_fee_close": fee_info["close_fee"], "est_fee_close": fee_info["close_fee"],
"est_fee_close_type": fee_info["close_type"], "est_fee_close_type": fee_info["close_type"],
"fee_source": fee_info.get("fee_source") or "local", "fee_source": fee_source,
"est_pnl_net": est_net, "est_pnl_net": est_net,
"sl_order_active": order_st.get("sl_monitoring"), "sl_order_active": order_st.get("sl_monitoring"),
"tp_order_active": order_st.get("tp_monitoring"), "tp_order_active": order_st.get("tp_monitoring"),
@@ -1082,6 +1169,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
if ctp_st.get("connected"): if ctp_st.get("connected"):
_ensure_monitors_from_ctp(conn, mode) _ensure_monitors_from_ctp(conn, mode)
rows = _build_trading_live_rows(conn, fast=fast) rows = _build_trading_live_rows(conn, fast=fast)
rows = _apply_account_margin_to_rows(rows, mode, capital)
pending_orders = _build_pending_orders(conn, mode) pending_orders = _build_pending_orders(conn, mode)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode)) risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode))
return { return {
+1 -1
View File
@@ -926,7 +926,7 @@
} }
return ''; return '';
}()); }());
var feeLabel = row.fee_source === 'ctp' ? '手续费(柜台)' : '手续费'; var feeLabel = row.fee_source === 'ctp' ? '已扣手续费(柜台)' : '已扣手续费';
var marginLabel = row.margin_source === 'ctp' ? '占用保证金(柜台)' : '占用保证金'; var marginLabel = row.margin_source === 'ctp' ? '占用保证金(柜台)' : '占用保证金';
var openLabel = '开仓'; var openLabel = '开仓';
return ( return (
+37 -1
View File
@@ -251,7 +251,21 @@ class CtpBridge:
except ImportError: except ImportError:
return return
def _on_position(_event) -> None: def _on_position(event) -> None:
try:
pos = event.data
vol = int(getattr(pos, "volume", 0) or 0)
if vol <= 0:
return
sym = getattr(pos, "symbol", "") or ""
d = "long" if _is_long_direction(getattr(pos, "direction", None)) else "short"
for attr in ("margin", "use_margin", "UseMargin"):
raw = float(getattr(pos, attr, 0) or 0)
if raw > 0:
self._position_margins[self._position_margin_key(sym, d)] = raw
break
except Exception as exc:
logger.debug("position margin cache: %s", exc)
_fire_position_refresh_callback_debounced() _fire_position_refresh_callback_debounced()
self._ee.register(EVENT_POSITION, _on_position) self._ee.register(EVENT_POSITION, _on_position)
@@ -1177,6 +1191,7 @@ class CtpBridge:
"lots": vol, "lots": vol,
"price": float(getattr(trade, "price", 0) or 0), "price": float(getattr(trade, "price", 0) or 0),
"datetime": dt, "datetime": dt,
"commission": round(float(getattr(trade, "commission", 0) or 0), 2),
} }
except Exception as exc: except Exception as exc:
logger.debug("trade_row_from_vnpy: %s", exc) logger.debug("trade_row_from_vnpy: %s", exc)
@@ -1216,6 +1231,9 @@ class CtpBridge:
"lots": vol, "lots": vol,
"price": price, "price": price,
"datetime": dt, "datetime": dt,
"commission": round(
float(data.get("Commission") or data.get("commission") or 0), 2,
),
} }
except Exception as exc: except Exception as exc:
logger.debug("trade_row_from_ctp_dict: %s", exc) logger.debug("trade_row_from_ctp_dict: %s", exc)
@@ -1476,6 +1494,24 @@ def ctp_get_account(mode: str) -> dict[str, Any]:
return b.get_account() return b.get_account()
def ctp_account_margin_used(mode: str) -> Optional[float]:
"""账户实际占用保证金 ≈ 权益 − 可用(与顶栏柜台资金一致)。"""
b = get_bridge()
if b.connected_mode != mode or not b.ping():
return None
try:
acc = b.get_account()
balance = float(acc.get("balance") or 0)
available = float(acc.get("available") or 0)
if balance <= 0:
return None
used = balance - available
return round(used, 2) if used > 0 else None
except Exception as exc:
logger.debug("ctp_account_margin_used: %s", exc)
return None
def ctp_list_positions( def ctp_list_positions(
mode: str, mode: str,
*, *,