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 <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-03 09:22:17 +08:00
parent 50bb04e2bb
commit 1cd3039605
3 changed files with 87 additions and 24 deletions
+12 -3
View File
@@ -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,
+42 -3
View File
@@ -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}%"
+33 -18
View File
@@ -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)