Align position margin with account balance and show deducted open commission only.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+92
-4
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user