From 1cd303960583dd7be553ef275a0747af3a376984 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 3 Jul 2026 09:22:17 +0800 Subject: [PATCH] Fix roll margin validation and SHFE close offset for night positions. Use CTP account margin for roll cap checks and prefer close-today on SHFE when yesterday close is rejected. Co-authored-by: Cursor --- modules/ctp/vnpy_bridge.py | 15 +++++++-- modules/trading/install.py | 45 ++++++++++++++++++++++++-- modules/trading/position_sizing.py | 51 +++++++++++++++++++----------- 3 files changed, 87 insertions(+), 24 deletions(-) diff --git a/modules/ctp/vnpy_bridge.py b/modules/ctp/vnpy_bridge.py index 0f4bbe7..ce8e722 100644 --- a/modules/ctp/vnpy_bridge.py +++ b/modules/ctp/vnpy_bridge.py @@ -1015,10 +1015,19 @@ class CtpBridge: return Offset.CLOSE vol = int(getattr(pos, "volume", 0) or 0) yd = int(getattr(pos, "yd_volume", 0) or 0) - today = max(0, vol - yd) - if today >= lots: + td = max(0, vol - yd) + if td >= lots: return Offset.CLOSETODAY - return Offset.CLOSEYESTERDAY + if yd >= lots: + # 夜盘仓在 vnpy 常记在 yd,日盘平昨易报「平昨仓位不足」→ 上期所/能源优先平今 + if ex_u in ("SHFE", "INE"): + return Offset.CLOSETODAY + return Offset.CLOSEYESTERDAY + if td + yd >= lots: + return Offset.CLOSETODAY + if ex_u in ("SHFE", "INE", "CZCE"): + return Offset.CLOSETODAY + return Offset.CLOSE def _aggressive_limit_price( self, diff --git a/modules/trading/install.py b/modules/trading/install.py index 525175d..20db87b 100644 --- a/modules/trading/install.py +++ b/modules/trading/install.py @@ -28,6 +28,7 @@ from modules.trading.position_sizing import ( calc_lots_by_risk, calc_margin_usage_pct, cap_lots_for_margin_budget, + current_margin_usage_pct, calc_order_tick_metrics, normalize_sizing_mode, ) @@ -1973,6 +1974,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se ).fetchone()["n"] if int(pending_n or 0) > 0: return False, "已有监控中的加仓腿" + cap_err = _validate_roll_margin( + conn, mode=mode, mon=mon, preview=preview, capital=_capital(conn), + ) + if cap_err: + return False, cap_err try: result = execute_order( conn, @@ -4508,8 +4514,15 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se conn = get_db() init_strategy_tables(conn) mode = get_trading_mode(get_setting) + capital = _capital(conn) + preview2, merr = _apply_roll_margin_cap( + preview, conn=conn, mode=mode, mon=mon, capital=capital, + ) + if merr: + conn.close() + return False, merr ok, msg = _commit_roll_fill( - conn, mon=mon, preview=preview, add_mode=leg.get("add_mode") or ADD_MODE_MARKET, + conn, mon=mon, preview=preview2, add_mode=leg.get("add_mode") or ADD_MODE_MARKET, mode=mode, pending_leg_id=int(leg["id"]), ) conn.close() @@ -4536,6 +4549,20 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se app._check_roll_monitors = _check_roll_monitors + def _validate_roll_margin( + conn, + *, + mode: str, + mon: dict, + preview: dict, + capital: float, + ) -> Optional[str]: + """滚仓提交前:用柜台保证金复核是否超过滚仓上限。""" + _, merr = _apply_roll_margin_cap( + preview, conn=conn, mode=mode, mon=mon, capital=capital, + ) + return merr + def _apply_roll_margin_cap( preview: dict, *, @@ -4558,9 +4585,21 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se mult = int(get_contract_spec(sym).get("mult") or 1) roll_pct = get_roll_max_margin_pct(get_setting) add_lots = int(preview.get("add_lots") or 0) - positions = _positions_for_monitor_restore(mode, allow_ctp=False) + positions = _positions_for_monitor_restore(mode, allow_ctp=True) + acct_margin = 0.0 + if ctp_status(mode).get("connected"): + acct_margin = float(ctp_account_margin_used(mode) or 0) + current_usage = current_margin_usage_pct( + positions, capital, trading_mode=mode, account_margin_used=acct_margin, + ) + if current_usage > roll_pct: + return preview, ( + f"当前保证金占用 {current_usage:g}% 已超过滚仓上限 {roll_pct:g}%," + "请先减仓或提高上限后再加仓" + ) capped, usage = cap_lots_for_margin_budget( - positions, capital, sym, direction, price, add_lots, roll_pct, trading_mode=mode, + positions, capital, sym, direction, price, add_lots, roll_pct, + trading_mode=mode, account_margin_used=acct_margin, ) if capped < 1: return preview, f"滚仓后保证金占用将超过上限 {roll_pct:g}%" diff --git a/modules/trading/position_sizing.py b/modules/trading/position_sizing.py index cea3621..06d2689 100644 --- a/modules/trading/position_sizing.py +++ b/modules/trading/position_sizing.py @@ -233,6 +233,22 @@ def calc_margin_usage_pct( return round(total / cap * 100.0, 2) +def current_margin_usage_pct( + positions: list[dict], + capital: float, + *, + trading_mode: str | None = None, + account_margin_used: float | None = None, +) -> float: + """当前保证金占权益(%);优先采用柜台账户占用,避免本地估算偏低。""" + cap = float(capital or 0) + usage = calc_margin_usage_pct(positions, cap, trading_mode=trading_mode) + acct = float(account_margin_used or 0) + if cap > 0 and acct > 0: + usage = max(usage, round(acct / cap * 100.0, 2)) + return usage + + def cap_lots_for_margin_budget( positions: list[dict], capital: float, @@ -242,29 +258,28 @@ def cap_lots_for_margin_budget( desired_lots: int, max_margin_pct: float, trading_mode: str | None = None, + account_margin_used: float | None = None, ) -> tuple[int, float]: """在保证金上限内,返回可加仓手数及占用比例。""" + cap = float(capital or 0) desired = max(0, int(desired_lots or 0)) + baseline_usage = current_margin_usage_pct( + positions, cap, trading_mode=trading_mode, account_margin_used=account_margin_used, + ) if desired <= 0: - return 0, calc_margin_usage_pct(positions, capital, trading_mode=trading_mode) + return 0, baseline_usage + if cap <= 0: + return 0, baseline_usage + baseline_margin = baseline_usage / 100.0 * cap + per_lot = 0.0 for lots in range(desired, 0, -1): - usage = calc_margin_usage_pct( - positions, - capital, - extra_symbol=symbol, - extra_lots=lots, - extra_price=price, - extra_direction=direction, - trading_mode=trading_mode, + per_lot, _, _ = margin_one_lot( + symbol, price, direction=direction, trading_mode=trading_mode, ) + if per_lot <= 0: + spec = get_contract_spec(symbol) + per_lot = price * spec["mult"] * spec["margin_rate"] + usage = round((baseline_margin + per_lot * lots) / cap * 100.0, 2) if usage <= max_margin_pct: return lots, usage - return 0, calc_margin_usage_pct( - positions, - capital, - extra_symbol=symbol, - extra_lots=desired, - extra_price=price, - extra_direction=direction, - trading_mode=trading_mode, - ) + return 0, round((baseline_margin + per_lot * desired) / cap * 100.0, 2)