From 27a51b4d1982d275ddb177500527e98e332ba06a Mon Sep 17 00:00:00 2001 From: dev-worker Date: Mon, 2 Mar 2026 16:11:43 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20P0=E7=AC=AC=E4=BA=8C=E8=BD=AE=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20=E2=80=94=20JWT=E5=AE=89=E5=85=A8/DB=E5=AF=86?= =?UTF-8?q?=E7=A0=81/SL=E7=B4=A7=E6=80=A5=E5=B9=B3=E4=BB=93reduceOnly/TP1?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=AE=88=E5=8D=AB/=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E7=B2=BE=E5=BA=A6/=E8=B7=A8=E7=AD=96=E7=95=A5=E5=8E=BB?= =?UTF-8?q?=E9=87=8D=20+=20=E7=A1=AC=E7=BC=96=E7=A0=81=E6=B6=88=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0-1: JWT_SECRET生产环境强制配置,测试环境保留默认 P0-2: DB密码生产环境强制从env读,测试环境保留fallback P0-3: SL三次失败→查真实持仓→reduceOnly平仓→校验结果→写event P0-4: TP1后SL重挂失败则不推进tp1_hit状态,continue等下轮重试 P0-5: 超时自动平仓用SYMBOL_QTY_PRECISION格式化+校验结果 P0-6: 同币种去重改为不区分策略(币安单向模式共享净仓位) P1-1: 手续费窗口entry_ts-200→+200(避免纳入开仓前成交) 额外: 模拟盘*200和实盘*2硬编码→从配置动态读取 --- backend/auth.py | 6 +++++- backend/live_executor.py | 41 ++++++++++++++++++++++++++++++++-------- backend/main.py | 10 ++++++---- backend/position_sync.py | 25 ++++++++++++++++++------ backend/risk_guard.py | 18 ++++++++++++++---- 5 files changed, 77 insertions(+), 23 deletions(-) diff --git a/backend/auth.py b/backend/auth.py index 5413a49..4579141 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -12,7 +12,11 @@ from pydantic import BaseModel, EmailStr from db import get_sync_conn -JWT_SECRET = os.getenv("JWT_SECRET", "arb-engine-jwt-secret-v2-2026") +_TRADE_ENV = os.getenv("TRADE_ENV", "testnet") +_jwt_default = "arb-engine-jwt-secret-v2-2026" if _TRADE_ENV == "testnet" else None +JWT_SECRET = os.getenv("JWT_SECRET") or _jwt_default +if not JWT_SECRET or len(JWT_SECRET) < 32: + raise RuntimeError("JWT_SECRET 未配置或长度不足(>=32),生产环境必须设置环境变量") ACCESS_TOKEN_HOURS = 24 REFRESH_TOKEN_DAYS = 7 diff --git a/backend/live_executor.py b/backend/live_executor.py index 9b259ce..11e3ba6 100644 --- a/backend/live_executor.py +++ b/backend/live_executor.py @@ -41,12 +41,17 @@ BINANCE_ENDPOINTS = { BASE_URL = BINANCE_ENDPOINTS[TRADE_ENV] # 数据库 +_DB_PASSWORD = os.getenv("DB_PASSWORD", "arb_engine_2026" if TRADE_ENV == "testnet" else "") +if not _DB_PASSWORD: + print("FATAL: DB_PASSWORD 未设置(生产环境必须配置)", file=sys.stderr) + sys.exit(1) + DB_CONFIG = { "host": os.getenv("DB_HOST", "10.106.0.3"), "port": int(os.getenv("DB_PORT", "5432")), "dbname": os.getenv("DB_NAME", "arb_engine"), "user": os.getenv("DB_USER", "arb"), - "password": os.getenv("DB_PASSWORD", "arb_engine_2026"), + "password": _DB_PASSWORD, } # 策略 @@ -354,10 +359,11 @@ async def execute_entry(session: aiohttp.ClientSession, signal: dict, db_conn): logger.warning(f"[{symbol}] ❌ 已达最大持仓数 {MAX_POSITIONS}") return None - # 3. 检查是否已有同币种同方向持仓 - cur.execute("SELECT id FROM live_trades WHERE symbol=%s AND strategy=%s AND status='active'", (symbol, strategy)) - if cur.fetchone(): - logger.info(f"[{symbol}] ⏭ 已有活跃持仓,跳过") + # 3. 检查是否已有同币种持仓(不区分策略,币安单向模式下同币共享净仓位) + cur.execute("SELECT id, strategy FROM live_trades WHERE symbol=%s AND status IN ('active', 'tp1_hit')", (symbol,)) + existing = cur.fetchone() + if existing: + logger.info(f"[{symbol}] ⏭ 已有活跃持仓(id={existing[0]}, strategy={existing[1]}),跳过") return None # 4. 设置杠杆和逐仓 @@ -419,9 +425,28 @@ async def execute_entry(session: aiohttp.ClientSession, signal: dict, db_conn): if sl_status == 200: break if sl_status != 200: - logger.error(f"[{symbol}] ❌ SL 3次全部失败,紧急市价平仓! data={sl_data}") - await place_market_order(session, symbol, close_side, qty) - _log_event(db_conn, "critical", "trade", f"SL挂单3次失败,已紧急平仓", symbol, {"sl_data": str(sl_data)}) + logger.error(f"[{symbol}] ❌ SL 3次全部失败,紧急reduceOnly平仓! data={sl_data}") + # 查真实持仓量,用reduceOnly市价平仓(避免反向开仓) + emergency_pos = await get_position(session, symbol) + if emergency_pos: + emergency_amt = abs(float(emergency_pos.get("positionAmt", 0))) + emergency_prec = SYMBOL_PRECISION.get(symbol, {"qty": 3}) + emergency_qty_str = f"{emergency_amt:.{emergency_prec['qty']}f}" + close_data, close_status = await binance_request(session, "POST", "/fapi/v1/order", { + "symbol": symbol, "side": close_side, "type": "MARKET", + "quantity": emergency_qty_str, "reduceOnly": "true", + }) + if close_status != 200: + logger.error(f"[{symbol}] ❌ 紧急平仓也失败! close_data={close_data}") + _log_event(db_conn, "critical", "trade", + f"SL失败后紧急平仓也失败,需人工介入", symbol, + {"sl_data": str(sl_data), "close_data": str(close_data)}) + else: + logger.info(f"[{symbol}] ✅ 紧急reduceOnly平仓成功 qty={emergency_qty_str}") + _log_event(db_conn, "critical", "trade", f"SL挂单3次失败,已紧急reduceOnly平仓", symbol, {"sl_data": str(sl_data)}) + else: + logger.warning(f"[{symbol}] SL失败但币安已无持仓,无需平仓") + _log_event(db_conn, "critical", "trade", f"SL挂单3次失败,但币安无持仓", symbol, {"sl_data": str(sl_data)}) return None t_after_sl = time.time() * 1000 protection_gap_ms = int(t_after_sl - t_fill) diff --git a/backend/main.py b/backend/main.py index 88b6d51..46392c7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -683,8 +683,9 @@ async def paper_summary( total = len(closed) wins = len([r for r in closed if r["pnl_r"] > 0]) total_pnl = sum(r["pnl_r"] for r in closed) - total_pnl_usdt = total_pnl * 200 # 1R = $200 - balance = 10000 + total_pnl_usdt + paper_1r_usd = paper_config["initial_balance"] * paper_config["risk_per_trade"] + total_pnl_usdt = total_pnl * paper_1r_usd + balance = paper_config["initial_balance"] + total_pnl_usdt win_rate = (wins / total * 100) if total > 0 else 0 gross_profit = sum(r["pnl_r"] for r in closed if r["pnl_r"] > 0) gross_loss = abs(sum(r["pnl_r"] for r in closed if r["pnl_r"] <= 0)) @@ -760,7 +761,8 @@ async def paper_positions( else: d["unrealized_pnl_r"] = round((entry - current_price) / rd, 2) # 浮动盈亏(USDT) — 假设1R = risk_per_trade - d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * 200, 2) # 2% of 10000 + paper_1r = paper_config["initial_balance"] * paper_config["risk_per_trade"] + d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * paper_1r, 2) else: d["unrealized_pnl_r"] = 0 d["unrealized_pnl_usdt"] = 0 @@ -1278,7 +1280,7 @@ async def live_positions( d["unrealized_pnl_r"] = round((current_price - entry) / rd, 4) else: d["unrealized_pnl_r"] = round((entry - current_price) / rd, 4) - d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * 2, 2) + d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * (await _get_risk_usd()), 2) else: d["unrealized_pnl_r"] = 0 d["unrealized_pnl_usdt"] = 0 diff --git a/backend/position_sync.py b/backend/position_sync.py index f0b4571..18b9d48 100644 --- a/backend/position_sync.py +++ b/backend/position_sync.py @@ -34,12 +34,17 @@ BINANCE_ENDPOINTS = { } BASE_URL = BINANCE_ENDPOINTS[TRADE_ENV] +_DB_PASSWORD = os.getenv("DB_PASSWORD", "arb_engine_2026" if TRADE_ENV == "testnet" else "") +if not _DB_PASSWORD: + print("FATAL: DB_PASSWORD 未设置(生产环境必须配置)", file=sys.stderr) + sys.exit(1) + DB_CONFIG = { "host": os.getenv("DB_HOST", "10.106.0.3"), "port": int(os.getenv("DB_PORT", "5432")), "dbname": os.getenv("DB_NAME", "arb_engine"), "user": os.getenv("DB_USER", "arb"), - "password": os.getenv("DB_PASSWORD", "arb_engine_2026"), + "password": _DB_PASSWORD, } CHECK_INTERVAL = 30 # 对账间隔(秒) @@ -368,20 +373,28 @@ async def check_tp1_triggers(session, conn): else: new_sl = lp["entry_price"] * 0.9995 - # 挂新SL(半仓) + # 挂新SL(半仓)— 失败则不推进状态 prec = SYMBOL_PRECISION.get(symbol, {"qty": 3, "price": 2}) - ok, _ = await rehang_sl(session, symbol, lp["direction"], new_sl, bp["amount"]) + ok, sl_resp = await rehang_sl(session, symbol, lp["direction"], new_sl, bp["amount"]) + if not ok: + logger.error(f"[{symbol}] ❌ TP1后重挂SL失败: {sl_resp},不推进tp1_hit状态") + _log_event(conn, "critical", "trade", + "TP1后重挂SL失败,仓位可能裸奔,需人工确认", symbol, + {"trade_id": lp["id"], "sl_resp": str(sl_resp)}) + continue # 重新挂TP2(半仓) tp2_price = lp["tp2_price"] qty_str = f"{abs(bp['amount']):.{prec['qty']}f}" price_str = f"{tp2_price:.{prec['price']}f}" - await binance_request(session, "POST", "/fapi/v1/order", { + tp2_data, tp2_status = await binance_request(session, "POST", "/fapi/v1/order", { "symbol": symbol, "side": close_side, "type": "TAKE_PROFIT_MARKET", "stopPrice": price_str, "quantity": qty_str, "reduceOnly": "true", }) + if tp2_status != 200: + logger.error(f"[{symbol}] ❌ TP2重挂失败: {tp2_data},SL已挂但TP2缺失") - # 更新DB + # SL成功才更新DB cur.execute(""" UPDATE live_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit' WHERE id=%s @@ -436,7 +449,7 @@ async def check_closed_positions(session, conn): # 汇总手续费(开仓后200ms起算,避免含其他策略成交) for t in trades_data: t_time = int(t.get("time", 0)) - if t_time >= entry_ts - 200: + if t_time >= entry_ts + 200: # 开仓后200ms起算,避免纳入开仓前成交 actual_fee_usdt += abs(float(t.get("commission", 0))) # 计算pnl — gross(不含费) diff --git a/backend/risk_guard.py b/backend/risk_guard.py index 5e6407c..931c622 100644 --- a/backend/risk_guard.py +++ b/backend/risk_guard.py @@ -39,12 +39,17 @@ BINANCE_ENDPOINTS = { } BASE_URL = BINANCE_ENDPOINTS[TRADE_ENV] +_DB_PASSWORD = os.getenv("DB_PASSWORD", "arb_engine_2026" if TRADE_ENV == "testnet" else "") +if not _DB_PASSWORD: + print("FATAL: DB_PASSWORD 未设置(生产环境必须配置)", file=sys.stderr) + sys.exit(1) + DB_CONFIG = { "host": os.getenv("DB_HOST", "10.106.0.3"), "port": int(os.getenv("DB_PORT", "5432")), "dbname": os.getenv("DB_NAME", "arb_engine"), "user": os.getenv("DB_USER", "arb"), - "password": os.getenv("DB_PASSWORD", "arb_engine_2026"), + "password": _DB_PASSWORD, } # 风控参数 @@ -279,11 +284,16 @@ async def check_hold_timeout(session, conn): for p in pos_data: amt = abs(float(p.get("positionAmt", 0))) if amt > 0 and p["symbol"] == symbol: - await binance_request(session, "POST", "/fapi/v1/order", { + qty_prec = SYMBOL_QTY_PRECISION.get(symbol, 3) + qty_str = f"{amt:.{qty_prec}f}" + close_data, close_status = await binance_request(session, "POST", "/fapi/v1/order", { "symbol": symbol, "side": close_side, "type": "MARKET", - "quantity": str(amt), "reduceOnly": "true", + "quantity": qty_str, "reduceOnly": "true", }) - logger.info(f"[{symbol}] 🔴 自动平仓完成 qty={amt}") + if close_status != 200: + logger.error(f"[{symbol}] ❌ 超时自动平仓失败: {close_data}") + else: + logger.info(f"[{symbol}] 🔴 超时自动平仓完成 qty={qty_str}") del risk_state.timeout_queue[trade_id] elif hold_min >= HOLD_TIMEOUT_RED_MIN: