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_cancel_order,
ctp_connect,
ctp_account_margin_used,
ctp_estimate_margin_one_lot,
ctp_get_account,
ctp_get_tick_price,
ctp_list_active_orders,
ctp_list_positions,
ctp_list_trades,
ctp_status,
execute_order,
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 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:
"""CTP 有持仓但本地无监控时,自动补写一条 active 记录供展示。"""
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
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:
try:
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(
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
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(
direction, entry, sl if sl is not None else entry,
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,
"rr_ratio": pos_metrics.get("rr_ratio") if sl is not None and tp is not None else None,
"float_pnl": float_pnl,
"est_fee": fee_info["total_fee"],
"est_fee_open": fee_info["open_fee"],
"est_fee": display_fee,
"est_fee_open": display_fee,
"est_fee_close": fee_info["close_fee"],
"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,
"sl_order_active": order_st.get("sl_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"):
_ensure_monitors_from_ctp(conn, mode)
rows = _build_trading_live_rows(conn, fast=fast)
rows = _apply_account_margin_to_rows(rows, mode, capital)
pending_orders = _build_pending_orders(conn, mode)
risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode))
return {