fix: 交易安全审计修复 — 补偿平仓、中控同步、滚仓/趋势防护

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-04 22:44:16 +08:00
parent df28e6dfb8
commit eb975b0133
11 changed files with 675 additions and 162 deletions
+120 -5
View File
@@ -130,6 +130,9 @@ from lib.key_monitor.trigger_entry_key_monitor_lib import (
TRIGGER_ENTRY_VALIDITY_HOURS,
check_trigger_entry_intent_limit,
count_pending_trigger_entries,
acquire_trigger_entry_exec_lock,
is_trigger_entry_in_flight_row,
release_trigger_entry_exec_lock,
is_breakout_trigger_entry_key_monitor_type,
is_trigger_entry_expired,
is_trigger_entry_key_monitor_type,
@@ -3469,6 +3472,40 @@ def ensure_markets_loaded(force=False):
MARKETS_LOADED = True
def _abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, planned_amount):
from lib.trade.compensating_close_lib import run_compensating_close
def _close():
ensure_markets_loaded()
try:
cancel_binance_futures_open_orders(exchange_symbol)
except Exception:
pass
live = get_live_position_contracts(exchange_symbol, direction)
amt = live if live is not None and live > 0 else _filled_amount_for_tpsl(order, planned_amount)
if amt is None or float(amt) <= 0:
return
side = "sell" if direction == "long" else "buy"
try:
amount = float(exchange.amount_to_precision(exchange_symbol, float(amt)))
except Exception:
amount = float(amt)
last_err = None
for params in _binance_market_close_param_candidates(direction):
try:
exchange.create_order(exchange_symbol, "market", side, amount, None, params)
return
except Exception as e:
last_err = e
if _is_binance_close_param_retryable(str(e)):
continue
raise
if last_err:
raise last_err
run_compensating_close(_close, log_prefix="binance_compensating_close")
def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss=None, take_profit=None):
ensure_markets_loaded()
mm = "cross" if BINANCE_MARGIN_MODE in ("cross", "cross_margin") else "isolated"
@@ -3487,8 +3524,10 @@ def place_exchange_order(exchange_symbol, direction, amount, leverage, stop_loss
_binance_place_tp_sl_orders(exchange_symbol, direction, pos_amt, stop_loss, take_profit)
order["tpsl_attached"] = True
except RuntimeError:
_abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, amount)
raise
except Exception as e:
_abort_market_open_after_tpsl_failure(exchange_symbol, direction, order, amount)
raise RuntimeError(f"交易所未接受条件止盈/止损委托,已拒绝开仓:{str(e)}") from e
return order
@@ -4495,6 +4534,72 @@ def resolve_synced_flat_close(row, opened_at_str, opened_at_ms=None):
)
def _finalize_hub_flat_monitor_binance(conn, r, *, result, pnl_amount, closed_at, miss_reason):
opened_at = get_opened_at_value(r)
closed_at_dt = parse_dt_for_trading_day(closed_at) or app_now()
hold_seconds = calc_hold_seconds(opened_at, closed_at_dt)
session_date = r["session_date"] or get_trading_day(closed_at_dt)
update_session_capital(conn, session_date, pnl_amount)
insert_trade_record(
conn,
symbol=r["symbol"],
monitor_type=trade_record_monitor_type(conn, r),
trend_plan_id=trend_plan_id_from_monitor_row(r),
key_signal_type=order_row_key_signal_type(r),
direction=r["direction"],
trigger_price=r["trigger_price"],
stop_loss=r["stop_loss"],
initial_stop_loss=r["initial_stop_loss"] or r["stop_loss"],
take_profit=r["take_profit"],
margin_capital=r["margin_capital"],
leverage=r["leverage"],
pnl_amount=pnl_amount,
hold_seconds=hold_seconds,
trade_style=r["trade_style"],
risk_amount=r["risk_amount"],
planned_rr=calc_rr_ratio(
r["direction"],
r["trigger_price"],
r["initial_stop_loss"] or r["stop_loss"],
r["take_profit"],
),
actual_rr=calc_actual_rr(pnl_amount, r["risk_amount"]),
result=result,
miss_reason=handoff_trade_miss_reason(miss_reason, r),
opened_at=opened_at,
closed_at=closed_at,
)
conn.execute("UPDATE order_monitors SET status='stopped' WHERE id=?", (r["id"],))
clear_key_sizing_snapshot_if_flat(conn, r["session_date"] or get_trading_day())
def reconcile_hub_external_close(conn, symbol, direction):
from lib.hub.hub_reconcile_flat_lib import reconcile_hub_external_close_impl
from lib.hub.hub_symbol_lib import symbols_match
global _RECONCILE_FLAT_STREAK
return reconcile_hub_external_close_impl(
conn,
symbol,
direction,
exchange_configured=exchange_private_api_configured,
not_configured_msg="未配置 BINANCE_API_KEY / BINANCE_API_SECRET",
symbols_match=symbols_match,
get_opened_at_value=get_opened_at_value,
resolve_monitor_exchange_symbol=resolve_monitor_exchange_symbol,
get_live_position_contracts=get_live_position_contracts,
cancel_conditional_orders=cancel_binance_futures_open_orders,
resolve_synced_flat_close=resolve_synced_flat_close,
finalize_stopped_monitor=_finalize_hub_flat_monitor_binance,
sync_trade_records=None,
reconcile_flat_streak=_RECONCILE_FLAT_STREAK,
to_ms_with_fallback=_to_ms_with_fallback,
prefer_manual_resolve=False,
order_row_monitor_type=order_row_monitor_type,
)
def reconcile_external_closes(conn, days=None):
global _RECONCILE_FLAT_STREAK
if not exchange_private_api_configured():
@@ -5777,7 +5882,7 @@ def _market_open_for_trigger_entry(
def _execute_trigger_entry_cross(conn, row):
"""标记价触达计划入场:先删监控行防重复触发,再市价开仓"""
"""标记价触达计划入场:加锁防重复触发,成交成功后再删监控行"""
symbol = row["symbol"]
direction = (row["direction"] or "long").lower()
ex_sym = normalize_exchange_symbol(symbol)
@@ -5788,7 +5893,8 @@ def _execute_trigger_entry_cross(conn, row):
tc_en, tc_h, _ = time_close_settings_from_row(row)
kid = int(row["id"])
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
if not acquire_trigger_entry_exec_lock(conn, kid):
return False, "触价开仓进行中"
conn.commit()
try:
@@ -5806,6 +5912,8 @@ def _execute_trigger_entry_cross(conn, row):
time_close_hours=tc_h,
)
except Exception as e:
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = friendly_exchange_error(e)
send_wechat_msg(
f"# ❌ {symbol} 触价开仓异常\n"
@@ -5817,6 +5925,8 @@ def _execute_trigger_entry_cross(conn, row):
return False, fail_msg
if ok and det:
conn.execute("DELETE FROM key_monitors WHERE id=?", (kid,))
conn.commit()
rr_txt = format_wechat_scalar_2dp(det.get("planned_rr_fill")) if det.get("planned_rr_fill") is not None else "-"
msg = (
f"# ✅ {symbol} 触价开仓成交\n"
@@ -5833,6 +5943,8 @@ def _execute_trigger_entry_cross(conn, row):
send_wechat_msg(msg)
insert_key_monitor_history(conn, row, 0, msg, TRIGGER_ENTRY_CLOSE_FILLED)
return True, None
release_trigger_entry_exec_lock(conn, kid)
conn.commit()
fail_msg = err or "触价触发后开仓失败"
send_wechat_msg(
f"# ❌ {symbol} 触价开仓失败\n"
@@ -5860,6 +5972,8 @@ def check_trigger_entry_key_monitors():
sl = float(_sqlite_row_val(r, "fib_stop_loss") or 0)
tp = float(_sqlite_row_val(r, "fib_take_profit") or 0)
kid = int(r["id"])
if is_trigger_entry_in_flight_row(r):
continue
if entry <= 0 or sl <= 0 or tp <= 0:
_finalize_key_monitor_one_shot(conn, r, "触价计划价位无效", "fib_plan_invalid")
continue
@@ -6388,7 +6502,7 @@ def check_order_monitors():
new_sl = round_price_to_exchange(ex_sym, new_sl)
tp_ex = float(take_profit or 0)
ok_live, _live_reason = ensure_exchange_live_ready()
synced_ex = not ok_live
synced_ex = False
if ok_live and tp_ex > 0:
try:
replace_active_monitor_tpsl_on_exchange(r, new_sl, tp_ex)
@@ -6818,8 +6932,8 @@ def background_task():
from lib.strategy.strategy_trend_register import check_trend_pullback_plans
check_trend_pullback_plans(cfg)
except:
pass
except Exception as e:
print(f"[monitor_loop] {e}", flush=True)
time.sleep(MONITOR_POLL_SECONDS)
@@ -9667,6 +9781,7 @@ try:
ohlcv_fn=_hub_fetch_ohlcv,
volume_rank_fn=_hub_fetch_volume_rank,
market_fn=_hub_fetch_market,
reconcile_hub_flat_fn=reconcile_hub_external_close,
risk_status_fn=hub_account_risk_status,
user_close_fn=hub_user_initiated_close,
render_main_page_fn=render_main_page,