diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index e00f01f..a28153f 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -1153,6 +1153,9 @@ async def sync_positions(account_id: int = Depends(get_account_id)): except Exception: exit_time_ts = None + # 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual + is_reduce_only = latest_close_order.get("reduceOnly", False) if latest_close_order else False + if "TRAILING" in otype: exit_reason = "trailing_stop" elif "TAKE_PROFIT" in otype: @@ -1160,7 +1163,44 @@ async def sync_positions(account_id: int = Depends(get_account_id)): elif "STOP" in otype: exit_reason = "stop_loss" elif otype in ("MARKET", "LIMIT"): - exit_reason = "manual" + # 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync,后续用价格判断 + if is_reduce_only: + exit_reason = "sync" # 临时标记,后续用价格判断 + else: + exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓 + + # 价格兜底:如果能明显命中止损/止盈价,则覆盖 exit_reason + # 这对于保护单触发的 MARKET 订单特别重要 + if exit_reason == "sync" or exit_reason == "manual": + try: + def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: + if a <= 0 or b <= 0: + return False + return abs((a - b) / b) <= max_pct + + ep = float(exit_price or 0) + if ep > 0: + sl = trade.get("stop_loss_price") + tp = trade.get("take_profit_price") + tp1 = trade.get("take_profit_1") + tp2 = trade.get("take_profit_2") + # 优先检查止损 + if sl is not None and _close_to(ep, float(sl), max_pct=0.02): + exit_reason = "stop_loss" + # 然后检查止盈 + elif tp is not None and _close_to(ep, float(tp), max_pct=0.02): + exit_reason = "take_profit" + elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02): + exit_reason = "take_profit" + elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02): + exit_reason = "take_profit" + # 如果价格接近入场价,可能是移动止损触发的 + elif is_reduce_only: + entry_price_val = float(trade.get("entry_price", 0) or 0) + if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): + exit_reason = "trailing_stop" + except Exception: + pass if not exit_price or exit_price <= 0: ticker = await client.get_ticker_24h(symbol) diff --git a/backend/api/routes/trades.py b/backend/api/routes/trades.py index a7dafc8..0cd117f 100644 --- a/backend/api/routes/trades.py +++ b/backend/api/routes/trades.py @@ -460,6 +460,9 @@ async def sync_trades_from_binance( # 细分 exit_reason:优先使用币安订单类型,其次用价格接近止损/止盈做兜底 exit_reason = "sync" + # 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual + is_reduce_only = order.get("reduceOnly", False) if isinstance(order, dict) else False + if "TRAILING" in otype: exit_reason = "trailing_stop" elif "TAKE_PROFIT" in otype: @@ -467,10 +470,14 @@ async def sync_trades_from_binance( elif "STOP" in otype: exit_reason = "stop_loss" elif otype in ("MARKET", "LIMIT"): - exit_reason = "manual" + # 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync,后续用价格判断 + if is_reduce_only: + exit_reason = "sync" # 临时标记,后续用价格判断 + else: + exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓 try: - def _close_to(a: float, b: float, max_pct: float = 0.01) -> bool: + def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: # 放宽到2%,因为滑点可能导致价格不完全一致 if a <= 0 or b <= 0: return False return abs((a - b) / b) <= max_pct @@ -481,14 +488,21 @@ async def sync_trades_from_binance( tp = trade.get("take_profit_price") tp1 = trade.get("take_profit_1") tp2 = trade.get("take_profit_2") - if sl is not None and _close_to(ep, float(sl), max_pct=0.01): + # 优先检查止损 + if sl is not None and _close_to(ep, float(sl), max_pct=0.02): exit_reason = "stop_loss" - elif tp is not None and _close_to(ep, float(tp), max_pct=0.01): + # 然后检查止盈 + elif tp is not None and _close_to(ep, float(tp), max_pct=0.02): exit_reason = "take_profit" - elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.01): + elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02): exit_reason = "take_profit" - elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.01): + elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02): exit_reason = "take_profit" + # 如果价格接近入场价,可能是移动止损触发的 + elif is_reduce_only and exit_reason == "sync": + entry_price_val = float(trade.get("entry_price", 0) or 0) + if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): + exit_reason = "trailing_stop" except Exception: pass diff --git a/backend/config_manager.py b/backend/config_manager.py index 294cdf4..2c96c15 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -541,11 +541,11 @@ class ConfigManager: 'STOP_LOSS_PERCENT': eff_get('STOP_LOSS_PERCENT', 0.10), # 默认10% 'TAKE_PROFIT_PERCENT': eff_get('TAKE_PROFIT_PERCENT', 0.25), # 默认25%(从30%放宽到25%,配合ATR止盈放大盈亏比) 'MIN_STOP_LOSS_PRICE_PCT': eff_get('MIN_STOP_LOSS_PRICE_PCT', 0.02), # 默认2% - 'MIN_TAKE_PROFIT_PRICE_PCT': eff_get('MIN_TAKE_PROFIT_PRICE_PCT', 0.03), # 默认3% + 'MIN_TAKE_PROFIT_PRICE_PCT': eff_get('MIN_TAKE_PROFIT_PRICE_PCT', 0.02), # 默认2%(防止ATR过小时计算出不切实际的微小止盈距离) 'USE_ATR_STOP_LOSS': eff_get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损 'ATR_STOP_LOSS_MULTIPLIER': eff_get('ATR_STOP_LOSS_MULTIPLIER', 1.8), # ATR止损倍数(1.5-2倍) - 'ATR_TAKE_PROFIT_MULTIPLIER': eff_get('ATR_TAKE_PROFIT_MULTIPLIER', 4.5), # ATR止盈倍数(从3.0提升到4.5,放大盈亏比) - 'RISK_REWARD_RATIO': eff_get('RISK_REWARD_RATIO', 3.0), # 盈亏比(止损距离的倍数) + 'ATR_TAKE_PROFIT_MULTIPLIER': eff_get('ATR_TAKE_PROFIT_MULTIPLIER', 1.5), # ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) + 'RISK_REWARD_RATIO': eff_get('RISK_REWARD_RATIO', 1.5), # 盈亏比(止损距离的倍数,从3.0降至1.5,更容易达成) 'ATR_PERIOD': eff_get('ATR_PERIOD', 14), # ATR计算周期 'USE_DYNAMIC_ATR_MULTIPLIER': eff_get('USE_DYNAMIC_ATR_MULTIPLIER', False), # 是否根据波动率动态调整ATR倍数 'ATR_MULTIPLIER_MIN': eff_get('ATR_MULTIPLIER_MIN', 1.5), # 动态ATR倍数最小值 diff --git a/backend/database/init.sql b/backend/database/init.sql index c6217bf..8e5a8b6 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -208,11 +208,11 @@ INSERT INTO `trading_config` (`config_key`, `config_value`, `config_type`, `cate ('STOP_LOSS_PERCENT', '0.10', 'number', 'risk', '止损:10%(相对于保证金)'), ('TAKE_PROFIT_PERCENT', '0.30', 'number', 'risk', '止盈:30%(相对于保证金,盈亏比3:1)'), ('MIN_STOP_LOSS_PRICE_PCT', '0.02', 'number', 'risk', '最小止损价格变动:2%(防止止损过紧)'), -('MIN_TAKE_PROFIT_PRICE_PCT', '0.03', 'number', 'risk', '最小止盈价格变动:3%(防止止盈过紧)'), +('MIN_TAKE_PROFIT_PRICE_PCT', '0.02', 'number', 'risk', '最小止盈价格变动:2%(防止ATR过小时计算出不切实际的微小止盈距离)'), ('USE_ATR_STOP_LOSS', 'true', 'boolean', 'risk', '是否使用ATR动态止损(优先于固定百分比)'), ('ATR_STOP_LOSS_MULTIPLIER', '1.8', 'number', 'risk', 'ATR止损倍数(1.5-2倍ATR,默认1.8)'), -('ATR_TAKE_PROFIT_MULTIPLIER', '3.0', 'number', 'risk', 'ATR止盈倍数(3倍ATR,对应3:1盈亏比)'), -('RISK_REWARD_RATIO', '3.0', 'number', 'risk', '盈亏比(止损距离的倍数,用于计算止盈)'), +('ATR_TAKE_PROFIT_MULTIPLIER', '1.5', 'number', 'risk', 'ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率)'), +('RISK_REWARD_RATIO', '1.5', 'number', 'risk', '盈亏比(止损距离的倍数,用于计算止盈,从3.0降至1.5,更容易达成)'), ('ATR_PERIOD', '14', 'number', 'risk', 'ATR计算周期(默认14)'), ('USE_DYNAMIC_ATR_MULTIPLIER', 'false', 'boolean', 'risk', '是否根据波动率动态调整ATR倍数'), ('ATR_MULTIPLIER_MIN', '1.5', 'number', 'risk', '动态ATR倍数最小值'), diff --git a/trading_system/atr_strategy.py b/trading_system/atr_strategy.py index bdbf085..1aa3c75 100644 --- a/trading_system/atr_strategy.py +++ b/trading_system/atr_strategy.py @@ -20,8 +20,8 @@ class ATRStrategy: """初始化ATR策略""" self.use_atr = config.TRADING_CONFIG.get('USE_ATR_STOP_LOSS', True) self.atr_sl_multiplier = config.TRADING_CONFIG.get('ATR_STOP_LOSS_MULTIPLIER', 1.8) - self.atr_tp_multiplier = config.TRADING_CONFIG.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0) - self.risk_reward_ratio = config.TRADING_CONFIG.get('RISK_REWARD_RATIO', 3.0) + self.atr_tp_multiplier = config.TRADING_CONFIG.get('ATR_TAKE_PROFIT_MULTIPLIER', 1.5) + self.risk_reward_ratio = config.TRADING_CONFIG.get('RISK_REWARD_RATIO', 1.5) self.atr_period = config.TRADING_CONFIG.get('ATR_PERIOD', 14) # 动态调整ATR倍数的参数 @@ -38,8 +38,8 @@ class ATRStrategy: cfg = getattr(config, "TRADING_CONFIG", {}) or {} self.use_atr = bool(cfg.get('USE_ATR_STOP_LOSS', True)) self.atr_sl_multiplier = float(cfg.get('ATR_STOP_LOSS_MULTIPLIER', 1.8)) - self.atr_tp_multiplier = float(cfg.get('ATR_TAKE_PROFIT_MULTIPLIER', 3.0)) - self.risk_reward_ratio = float(cfg.get('RISK_REWARD_RATIO', 3.0)) + self.atr_tp_multiplier = float(cfg.get('ATR_TAKE_PROFIT_MULTIPLIER', 1.5)) + self.risk_reward_ratio = float(cfg.get('RISK_REWARD_RATIO', 1.5)) self.atr_period = int(cfg.get('ATR_PERIOD', 14)) self.use_dynamic_atr_multiplier = bool(cfg.get('USE_DYNAMIC_ATR_MULTIPLIER', False)) self.atr_multiplier_min = float(cfg.get('ATR_MULTIPLIER_MIN', 1.5)) diff --git a/trading_system/config.py b/trading_system/config.py index b3ad7a8..5fadf55 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -190,11 +190,11 @@ def _get_trading_config(): 'STOP_LOSS_PERCENT': 0.10, # 止损百分比(相对于保证金),默认10% 'TAKE_PROFIT_PERCENT': 0.25, # 止盈百分比(相对于保证金),默认25%(从30%放宽,配合ATR止盈放大盈亏比) 'MIN_STOP_LOSS_PRICE_PCT': 0.02, # 最小止损价格变动百分比(如0.02表示2%),防止止损过紧,默认2% - 'MIN_TAKE_PROFIT_PRICE_PCT': 0.03, # 最小止盈价格变动百分比(如0.03表示3%),防止止盈过紧,默认3% + 'MIN_TAKE_PROFIT_PRICE_PCT': 0.02, # 最小止盈价格变动百分比(如0.02表示2%),防止ATR过小时计算出不切实际的微小止盈距离,默认2% 'USE_ATR_STOP_LOSS': True, # 是否使用ATR动态止损(优先于固定百分比) 'ATR_STOP_LOSS_MULTIPLIER': 1.8, # ATR止损倍数(1.5-2倍ATR,默认1.8) - 'ATR_TAKE_PROFIT_MULTIPLIER': 4.5, # ATR止盈倍数(从3.0提升到4.5,放大盈亏比,让利润奔跑) - 'RISK_REWARD_RATIO': 3.0, # 盈亏比(止损距离的倍数,用于计算止盈) + 'ATR_TAKE_PROFIT_MULTIPLIER': 1.5, # ATR止盈倍数(从4.5降至1.5,将盈亏比从3:1降至更现实、可达成的1.5:1,提升止盈触发率) + 'RISK_REWARD_RATIO': 1.5, # 盈亏比(止损距离的倍数,用于计算止盈,从3.0降至1.5,更容易达成) 'ATR_PERIOD': 14, # ATR计算周期(默认14) 'USE_DYNAMIC_ATR_MULTIPLIER': False, # 是否根据波动率动态调整ATR倍数 'ATR_MULTIPLIER_MIN': 1.5, # 动态ATR倍数最小值 diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index f02b3f6..c8b8499 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -1466,13 +1466,13 @@ class PositionManager: # 分步止盈后的“保本”处理: # - 若启用 USE_TRAILING_STOP:允许把剩余仓位止损移至成本价,并进入移动止损阶段 # - 若关闭 USE_TRAILING_STOP:严格不自动移动止损(避免你说的“仍然保本/仍然移动止损”) - use_trailing = config.TRADING_CONFIG.get('USE_TRAILING_STOP', True) - if use_trailing: - position_info['stopLoss'] = entry_price - position_info['trailingStopActivated'] = True - logger.info(f"{symbol} 剩余仓位止损移至成本价(保本),配合移动止损博取更大利润") - else: - logger.info(f"{symbol} 已部分止盈,但已关闭移动止损:不自动将止损移至成本价") + # 无论是否启用移动止损,分步止盈后都将剩余仓位止损移至成本价(保本) + # 这样既不错失后续行情,又彻底杜绝了该笔交易亏损的可能 + position_info['stopLoss'] = entry_price + logger.info( + f"{symbol} 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本)," + f"剩余50%仓位追求1.5:1止盈目标" + ) else: # 兜底:可能遇到 -2022(reduceOnly rejected)等竞态,重新查一次持仓 try: @@ -1878,6 +1878,9 @@ class PositionManager: or latest_close_order.get("origType") or "" ).upper() + # 检查订单的 reduceOnly 字段:如果是 true,说明是自动平仓,不应该标记为 manual + is_reduce_only = latest_close_order.get("reduceOnly", False) + if "TRAILING" in otype: exit_reason = "trailing_stop" elif "TAKE_PROFIT" in otype: @@ -1886,7 +1889,11 @@ class PositionManager: # STOP / STOP_MARKET 通常对应止损触发 exit_reason = "stop_loss" elif otype in ("MARKET", "LIMIT"): - exit_reason = "manual" + # 如果是 reduceOnly 订单,说明是自动平仓(可能是保护单触发的),先标记为 sync,后续用价格判断 + if is_reduce_only: + exit_reason = "sync" # 临时标记,后续用价格判断 + else: + exit_reason = "manual" # 非 reduceOnly 的 MARKET/LIMIT 订单才是真正的手动平仓 ms = latest_close_order.get("updateTime") or latest_close_order.get("time") try: @@ -1900,8 +1907,9 @@ class PositionManager: pass # 价格兜底:如果能明显命中止损/止盈价,则覆盖 exit_reason + # 这对于保护单触发的 MARKET 订单特别重要(币安的保护单触发后会生成 MARKET 订单) try: - def _close_to(a: float, b: float, max_pct: float = 0.01) -> bool: + def _close_to(a: float, b: float, max_pct: float = 0.02) -> bool: # 放宽到2%,因为滑点可能导致价格不完全一致 if a <= 0 or b <= 0: return False return abs((a - b) / b) <= max_pct @@ -1912,14 +1920,22 @@ class PositionManager: tp = trade.get("take_profit_price") tp1 = trade.get("take_profit_1") tp2 = trade.get("take_profit_2") - if sl is not None and _close_to(ep, float(sl), max_pct=0.01): + # 优先检查止损(因为止损更关键) + if sl is not None and _close_to(ep, float(sl), max_pct=0.02): exit_reason = "stop_loss" - elif tp is not None and _close_to(ep, float(tp), max_pct=0.01): + # 然后检查止盈 + elif tp is not None and _close_to(ep, float(tp), max_pct=0.02): exit_reason = "take_profit" - elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.01): + elif tp1 is not None and _close_to(ep, float(tp1), max_pct=0.02): exit_reason = "take_profit" - elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.01): + elif tp2 is not None and _close_to(ep, float(tp2), max_pct=0.02): exit_reason = "take_profit" + # 如果之前标记为 sync 且是 reduceOnly 订单,但价格不匹配止损/止盈,可能是其他自动平仓(如移动止损) + elif exit_reason == "sync" and is_reduce_only: + # 检查是否是移动止损:如果价格接近入场价,可能是移动止损触发的 + entry_price_val = float(trade.get("entry_price", 0) or 0) + if entry_price_val > 0 and _close_to(ep, entry_price_val, max_pct=0.01): + exit_reason = "trailing_stop" except Exception: pass @@ -2555,7 +2571,92 @@ class PositionManager: logger.warning(f" 移动止损: 已激活(从初始止损 {position_info.get('initialStopLoss', 'N/A')} 调整)") logger.warning("=" * 80) - # 检查止盈(基于保证金收益比) + # 检查分步止盈(实时监控) + if not should_close: + take_profit_1 = position_info.get('takeProfit1') # 第一目标(盈亏比1:1) + take_profit_2 = position_info.get('takeProfit2', position_info.get('takeProfit')) # 第二目标(1.5:1) + partial_profit_taken = position_info.get('partialProfitTaken', False) + remaining_quantity = position_info.get('remainingQuantity', quantity) + + # 第一目标:盈亏比1:1,了结50%仓位 + if not partial_profit_taken and take_profit_1 is not None: + # 计算第一目标对应的保证金百分比 + if position_info['side'] == 'BUY': + take_profit_1_amount = (take_profit_1 - entry_price) * quantity + else: # SELL + take_profit_1_amount = (entry_price - take_profit_1) * quantity + take_profit_1_pct_margin = (take_profit_1_amount / margin * 100) if margin > 0 else 0 + + # 直接比较当前盈亏百分比与第一目标(基于保证金) + if pnl_percent_margin >= take_profit_1_pct_margin: + logger.info( + f"{symbol} [实时监控] 触发第一目标止盈(盈亏比1:1,基于保证金): " + f"当前盈亏={pnl_percent_margin:.2f}% of margin >= 目标={take_profit_1_pct_margin:.2f}% of margin | " + f"当前价={current_price_float:.4f}, 目标价={take_profit_1:.4f}" + ) + # 部分平仓50% + partial_quantity = quantity * 0.5 + try: + close_side = 'SELL' if position_info['side'] == 'BUY' else 'BUY' + close_position_side = 'LONG' if position_info['side'] == 'BUY' else 'SHORT' + live_amt = await self._get_live_position_amt(symbol, position_side=close_position_side) + if live_amt is None or abs(live_amt) <= 0: + logger.warning(f"{symbol} [实时监控] 部分止盈:实时持仓已为0,跳过部分平仓") + else: + partial_quantity = min(partial_quantity, abs(live_amt)) + partial_quantity = await self._adjust_close_quantity(symbol, partial_quantity) + if partial_quantity > 0: + partial_order = await self.client.place_order( + symbol=symbol, + side=close_side, + quantity=partial_quantity, + order_type='MARKET', + reduce_only=True, + position_side=close_position_side, + ) + if partial_order: + position_info['partialProfitTaken'] = True + position_info['remainingQuantity'] = remaining_quantity - partial_quantity + logger.info( + f"{symbol} [实时监控] 部分止盈成功: 平仓{partial_quantity:.4f},剩余{position_info['remainingQuantity']:.4f}" + ) + # 分步止盈后的"保本"处理:将剩余仓位止损移至成本价(保本) + position_info['stopLoss'] = entry_price + logger.info( + f"{symbol} [实时监控] 部分止盈后:剩余仓位止损移至成本价 {entry_price:.4f}(保本)," + f"剩余50%仓位追求1.5:1止盈目标" + ) + except Exception as e: + logger.error(f"{symbol} [实时监控] 部分止盈失败: {e}") + + # 第二目标:1.5:1止盈,平掉剩余仓位 + if partial_profit_taken and take_profit_2 is not None and not should_close: + # 计算第二目标对应的保证金百分比(基于剩余仓位) + if position_info['side'] == 'BUY': + take_profit_2_amount = (take_profit_2 - entry_price) * remaining_quantity + else: # SELL + take_profit_2_amount = (entry_price - take_profit_2) * remaining_quantity + remaining_margin = (entry_price * remaining_quantity) / leverage if leverage > 0 else (entry_price * remaining_quantity) + take_profit_2_pct_margin = (take_profit_2_amount / remaining_margin * 100) if remaining_margin > 0 else 0 + # 计算剩余仓位的当前盈亏 + if position_info['side'] == 'BUY': + remaining_pnl_amount = (current_price_float - entry_price) * remaining_quantity + else: + remaining_pnl_amount = (entry_price - current_price_float) * remaining_quantity + remaining_pnl_pct_margin = (remaining_pnl_amount / remaining_margin * 100) if remaining_margin > 0 else 0 + + # 直接比较剩余仓位盈亏百分比与第二目标(基于保证金) + if remaining_pnl_pct_margin >= take_profit_2_pct_margin: + should_close = True + exit_reason = 'take_profit' + logger.info( + f"{symbol} [实时监控] 触发第二目标止盈(1.5:1,基于保证金): " + f"剩余仓位盈亏={remaining_pnl_pct_margin:.2f}% of margin >= 目标={take_profit_2_pct_margin:.2f}% of margin | " + f"当前价={current_price_float:.4f}, 目标价={take_profit_2:.4f}, " + f"剩余数量={remaining_quantity:.4f}" + ) + + # 检查止盈(基于保证金收益比)- 用于未启用分步止盈的情况 if not should_close: take_profit = position_info.get('takeProfit') if take_profit is not None: