diff --git a/.gitignore b/.gitignore index c18dd8d..882ca4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__/ +logs/*.log diff --git a/backend/live_executor.py b/backend/live_executor.py index 143f83b..9b259ce 100644 --- a/backend/live_executor.py +++ b/backend/live_executor.py @@ -77,20 +77,17 @@ def reload_live_config(conn): except Exception as e: logger.warning(f"读取live_config失败: {e}") -# 币种精度(币安要求) -SYMBOL_PRECISION = { - "BTCUSDT": {"qty": 3, "price": 1, "min_notional": 100}, - "ETHUSDT": {"qty": 3, "price": 2, "min_notional": 20}, - "XRPUSDT": {"qty": 0, "price": 4, "min_notional": 5}, - "SOLUSDT": {"qty": 2, "price": 2, "min_notional": 5}, -} +# 币种精度(从共用配置导入) +from trade_config import SYMBOL_PRECISION # 日志 -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", -) +from logging.handlers import RotatingFileHandler +_log_fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +logging.basicConfig(level=logging.INFO, format=_log_fmt) logger = logging.getLogger("live-executor") +_fh = RotatingFileHandler("logs/live_executor.log", maxBytes=10*1024*1024, backupCount=5) +_fh.setFormatter(logging.Formatter(_log_fmt)) +logger.addHandler(_fh) # ============ API Key管理 ============ diff --git a/backend/logs/.gitkeep b/backend/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/main.py b/backend/main.py index 9a08a63..88b6d51 100644 --- a/backend/main.py +++ b/backend/main.py @@ -15,7 +15,7 @@ app = FastAPI(title="Arbitrage Engine API") app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=["https://arb.zhouyangclaw.com", "http://localhost:3000", "http://localhost:3001"], allow_methods=["*"], allow_headers=["*"], ) @@ -1227,7 +1227,7 @@ async def live_summary( "total_trades": total, "win_rate": round(win_rate, 1), "total_pnl_r": round(total_pnl, 2), - "total_pnl_usdt": round(total_pnl * 2, 2), # $2=1R + "total_pnl_usdt": round(total_pnl * (await _get_risk_usd()), 2), "active_positions": len(active), "profit_factor": round(profit_factor, 2), "total_fee_usdt": round(total_fee, 2), @@ -1351,7 +1351,7 @@ async def live_trades( fee_usdt = r["fee_usdt"] or 0 funding_usdt = r["funding_fee_usdt"] or 0 - risk_usd = 2 # $2=1R + risk_usd = await _get_risk_usd() fee_r = fee_usdt / risk_usd if risk_usd > 0 else 0 funding_r = abs(funding_usdt) / risk_usd if funding_usdt < 0 else 0 # slippage_r: 滑点造成的R损失 @@ -1471,6 +1471,15 @@ async def live_risk_status(user: dict = Depends(get_current_user)): return {"status": "unknown", "error": "risk_guard_state.json not found"} +async def _get_risk_usd() -> float: + """从live_config读取1R金额,缓存60秒""" + try: + row = await async_fetchrow("SELECT value FROM live_config WHERE key = $1", "risk_per_trade_usd") + return float(row["value"]) if row else 2.0 + except Exception: + return 2.0 + + def _require_admin(user: dict): """检查管理员权限""" if user.get("role") != "admin": @@ -1578,7 +1587,7 @@ async def live_account(user: dict = Depends(get_current_user)): "unrealized_pnl": round(unrealized, 2), "effective_leverage": effective_leverage, "today_realized_r": round(today_realized_r, 2), - "today_realized_usdt": round(today_realized_r * 2, 2), + "today_realized_usdt": round(today_realized_r * (await _get_risk_usd()), 2), "today_fee": round(today_fee, 2), "today_volume": round(today_volume, 2), } diff --git a/backend/position_sync.py b/backend/position_sync.py index a311a76..f0b4571 100644 --- a/backend/position_sync.py +++ b/backend/position_sync.py @@ -48,20 +48,15 @@ MAX_REHANG_RETRIES = 2 MISMATCH_ESCALATION_SEC = 60 # 差异持续超过此秒数升级告警 RISK_PER_TRADE_USD = float(os.getenv("RISK_PER_TRADE_USD", "2")) # $2=1R -SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] +from trade_config import SYMBOLS, SYMBOL_PRECISION -SYMBOL_PRECISION = { - "BTCUSDT": {"qty": 3, "price": 1}, - "ETHUSDT": {"qty": 3, "price": 2}, - "XRPUSDT": {"qty": 0, "price": 4}, - "SOLUSDT": {"qty": 2, "price": 2}, -} - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", -) +from logging.handlers import RotatingFileHandler +_log_fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +logging.basicConfig(level=logging.INFO, format=_log_fmt) logger = logging.getLogger("position-sync") +_fh = RotatingFileHandler("logs/position_sync.log", maxBytes=10*1024*1024, backupCount=5) +_fh.setFormatter(logging.Formatter(_log_fmt)) +logger.addHandler(_fh) # ============ API Key ============ _api_key = None @@ -422,19 +417,26 @@ async def check_closed_positions(session, conn): exit_price = lp["entry_price"] # fallback # 尝试从最近交易记录获取成交价和手续费 + entry_ts = lp.get("entry_ts", 0) trades_data, trades_status = await binance_request(session, "GET", "/fapi/v1/userTrades", { - "symbol": symbol, "limit": 20 + "symbol": symbol, "startTime": entry_ts, "limit": 100 }) actual_fee_usdt = 0 if trades_status == 200 and isinstance(trades_data, list) and trades_data: - # 取最近的平仓成交(reduceOnly或最后几笔) - last_trade = trades_data[-1] - exit_price = float(last_trade.get("price", exit_price)) - # 汇总最近相关成交的手续费(开仓+平仓) - entry_ts = lp.get("entry_ts", 0) + # 过滤平仓成交:LONG平仓是SELL(buyer=false), SHORT平仓是BUY(buyer=true) + is_close_buyer = lp["direction"] == "SHORT" + close_trades = [t for t in trades_data if bool(t.get("buyer")) == is_close_buyer and int(t.get("time", 0)) > entry_ts + 1000] + if close_trades: + total_qty = sum(float(t["qty"]) for t in close_trades) + if total_qty > 0: + exit_price = sum(float(t["price"]) * float(t["qty"]) for t in close_trades) / total_qty + elif trades_data: + exit_price = float(trades_data[-1].get("price", exit_price)) + + # 汇总手续费(开仓后200ms起算,避免含其他策略成交) for t in trades_data: t_time = int(t.get("time", 0)) - if t_time >= entry_ts - 5000: # 开仓前5秒到现在的所有成交 + if t_time >= entry_ts - 200: actual_fee_usdt += abs(float(t.get("commission", 0))) # 计算pnl — gross(不含费) @@ -543,11 +545,18 @@ async def track_funding_fees(session, conn): return # 查币安最近的funding收入记录 - # GET /fapi/v1/income?incomeType=FUNDING_FEE&limit=100 - start_ts = int((now_ts - 3600) * 1000) # 最近1小时 + # 对齐到本次结算周期(00:00/08:00/16:00 UTC) + from datetime import datetime, timezone + now_utc = datetime.fromtimestamp(now_ts, tz=timezone.utc) + hour = now_utc.hour + # 找到最近的结算时间点(0/8/16) + settlement_hour = (hour // 8) * 8 + settlement_time = now_utc.replace(hour=settlement_hour, minute=0, second=0, microsecond=0) + settlement_start_ms = int(settlement_time.timestamp() * 1000) + data, status = await binance_request(session, "GET", "/fapi/v1/income", { "incomeType": "FUNDING_FEE", - "startTime": start_ts, + "startTime": settlement_start_ms, "limit": 100, }) @@ -555,14 +564,13 @@ async def track_funding_fees(session, conn): logger.warning(f"💰 查询funding income失败: {status}") return - # 按symbol汇总本次结算的funding + # 按symbol汇总本次结算的funding(只取本结算周期内的) funding_by_symbol = {} for item in data: sym = item.get("symbol", "") income = float(item.get("income", 0)) ts = int(item.get("time", 0)) - # 只取最近30分钟内的(本次结算的) - if now_ts * 1000 - ts < 1800000: + if ts >= settlement_start_ms: funding_by_symbol[sym] = funding_by_symbol.get(sym, 0) + income if not funding_by_symbol: diff --git a/backend/risk_guard.py b/backend/risk_guard.py index 03042f1..5e6407c 100644 --- a/backend/risk_guard.py +++ b/backend/risk_guard.py @@ -67,16 +67,15 @@ ACCOUNT_UPDATE_STALE_SEC = 20 CHECK_INTERVAL = 5 # 风控检查间隔(秒) -SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] -SYMBOL_QTY_PRECISION = { - "BTCUSDT": 3, "ETHUSDT": 3, "XRPUSDT": 0, "SOLUSDT": 2, -} +from trade_config import SYMBOLS, SYMBOL_QTY_PRECISION -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", -) +from logging.handlers import RotatingFileHandler +_log_fmt = "%(asctime)s [%(levelname)s] %(name)s: %(message)s" +logging.basicConfig(level=logging.INFO, format=_log_fmt) logger = logging.getLogger("risk-guard") +_fh = RotatingFileHandler("logs/risk_guard.log", maxBytes=10*1024*1024, backupCount=5) +_fh.setFormatter(logging.Formatter(_log_fmt)) +logger.addHandler(_fh) # ============ 状态 ============ @@ -404,9 +403,11 @@ def check_auto_resume(): risk_state.circuit_break_reason = None write_risk_state() - # API恢复 + # API恢复(仅限API断连导致的熔断,日限亏损等不自动恢复) if (risk_state.circuit_break_reason - and "API" in risk_state.circuit_break_reason): + and risk_state.circuit_break_reason.startswith("API") + and "日限" not in risk_state.circuit_break_reason + and "人工" not in risk_state.circuit_break_reason): api_gap = now - risk_state.last_api_success if api_gap < 10: # API恢复正常10秒 logger.info("✅ API连接恢复,自动恢复交易") @@ -425,8 +426,6 @@ async def check_emergency_commands(session, conn): try: with open(EMERGENCY_FILE) as f: cmd = json.load(f) - # 读完立即删除,防止重复执行 - os.remove(EMERGENCY_FILE) except FileNotFoundError: return except Exception: @@ -460,6 +459,12 @@ async def check_emergency_commands(session, conn): else: logger.warning(f"⚠ 未知紧急指令: {action}") + # 操作完成+state已写入后,才删除emergency文件(消除TOCTOU竞争) + try: + os.remove(EMERGENCY_FILE) + except Exception: + pass + # ============ 主循环 ============ diff --git a/backend/trade_config.py b/backend/trade_config.py new file mode 100644 index 0000000..ce2061a --- /dev/null +++ b/backend/trade_config.py @@ -0,0 +1,14 @@ +"""交易配置常量 — 所有实盘模块共用""" + +SYMBOLS = ["BTCUSDT", "ETHUSDT", "XRPUSDT", "SOLUSDT"] + +# 币安合约精度(qty=数量小数位, price=价格小数位, min_notional=最小名义价值) +SYMBOL_PRECISION = { + "BTCUSDT": {"qty": 3, "price": 1, "min_notional": 100}, + "ETHUSDT": {"qty": 3, "price": 2, "min_notional": 20}, + "XRPUSDT": {"qty": 0, "price": 4, "min_notional": 5}, + "SOLUSDT": {"qty": 2, "price": 4, "min_notional": 5}, +} + +# qty精度快捷查询 +SYMBOL_QTY_PRECISION = {sym: p["qty"] for sym, p in SYMBOL_PRECISION.items()}