From a38f7cf09d2ec1880c55b85f8462d6cb7736d4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 20 Mar 2026 08:42:36 +0800 Subject: [PATCH] 1 --- backend/config_manager.py | 7 +++ trading_system/config.py | 10 ++++ trading_system/position_manager.py | 95 +++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 1 deletion(-) diff --git a/backend/config_manager.py b/backend/config_manager.py index 1e81490..8c254de 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -966,6 +966,13 @@ class ConfigManager: 'TREND_STATE_TTL_SEC': eff_get('TREND_STATE_TTL_SEC', 3600), 'RECO_USE_TREND_ENTRY_FILTER': eff_get('RECO_USE_TREND_ENTRY_FILTER', True), 'RECO_MAX_TREND_MOVE_BEFORE_ENTRY': eff_get('RECO_MAX_TREND_MOVE_BEFORE_ENTRY', 0.04), + # 回撤/区间入场(前低前高) + 'ENTRY_PULLBACK_FILTER_ENABLED': eff_get('ENTRY_PULLBACK_FILTER_ENABLED', False), + 'ENTRY_PULLBACK_INTERVAL': eff_get('ENTRY_PULLBACK_INTERVAL', None), + 'ENTRY_PULLBACK_LOOKBACK_BARS': eff_get('ENTRY_PULLBACK_LOOKBACK_BARS', 24), + 'ENTRY_PULLBACK_MIN_BARS': eff_get('ENTRY_PULLBACK_MIN_BARS', 5), + 'ENTRY_PULLBACK_MAX_LONG_IN_RANGE': eff_get('ENTRY_PULLBACK_MAX_LONG_IN_RANGE', 0.62), + 'ENTRY_PULLBACK_MIN_SHORT_IN_RANGE': eff_get('ENTRY_PULLBACK_MIN_SHORT_IN_RANGE', 0.38), # 当前交易预设(让 trading_system 能知道是哪种模式) 'TRADING_PROFILE': profile, diff --git a/trading_system/config.py b/trading_system/config.py index f3e4332..5cdeb8a 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -363,6 +363,16 @@ DEFAULT_TRADING_CONFIG = { # 推荐系统使用的最大允许趋势幅度(相对于趋势信号价),默认与自动交易相同或略微更严格 'RECO_MAX_TREND_MOVE_BEFORE_ENTRY': 0.04, + # ===== 回撤/区间入场过滤(参考近N根K线前低、前高,减少买在区间顶部、卖在区间底部)===== + # 与 USE_TREND_ENTRY_FILTER(相对「信号价」的追价幅度)互补:本组用 K 线区间衡量是否已跑到局部极值附近。 + 'ENTRY_PULLBACK_FILTER_ENABLED': False, # True 启用;建议先 True 观察日志中 [回撤过滤] 跳过比例再调阈值 + 'ENTRY_PULLBACK_INTERVAL': None, # None=沿用 ENTRY_INTERVAL(默认1h);可改为 '15m' 等更敏感周期 + 'ENTRY_PULLBACK_LOOKBACK_BARS': 24, # 统计区间用的 K 线根数(需 >= MIN_BARS) + 'ENTRY_PULLBACK_MIN_BARS': 5, # 至少多少根有效 K 才启用过滤,不足则跳过本过滤 + # 现价在 [区间低,区间高] 中的相对位置 pos∈[0,1]:0=贴前低,1=贴前高 + 'ENTRY_PULLBACK_MAX_LONG_IN_RANGE': 0.62, # 做多:pos 超过此值则不开(默认拒绝最上约 38% 区域) + 'ENTRY_PULLBACK_MIN_SHORT_IN_RANGE': 0.38, # 做空:pos 低于此值则不开(默认拒绝最下约 38% 区域) + # ===== 智能入场(方案C)===== # 根治方案:默认关闭。关闭后回归“纯限价单模式”(不追价/不市价兜底/未成交撤单跳过) 'SMART_ENTRY_ENABLED': True, # 开启智能入场,提高成交率 diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 4d3b9d1..2bb4656 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -8,7 +8,7 @@ import random import time import aiohttp from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple from datetime import datetime try: from .binance_client import BinanceClient, AlgoOrderPositionUnavailableError @@ -149,6 +149,46 @@ except Exception: get_balance_from_cache = lambda: None +def _pullback_range_metrics_from_klines( + klines: Optional[List], + current_price: float, +) -> Optional[Tuple[float, float, float]]: + """ + 用近若干根 K 线的最高价/最低价构成区间,衡量当前价在区间内的相对位置。 + + 返回 (recent_low, recent_high, position_01),其中 position_01∈[0,1]: + - 0 表示贴在区间下沿(接近「前低」一侧) + - 1 表示贴在区间上沿(接近「前高」一侧) + + 多单若 position_01 过大,说明价格已跑到区间顶部,容易「一回撤就扫损」; + 空单若 position_01 过小,同理接近区间下沿追空风险大。 + """ + if not klines or current_price is None: + return None + try: + cur = float(current_price) + except (TypeError, ValueError): + return None + if cur <= 0: + return None + highs: List[float] = [] + lows: List[float] = [] + for k in klines: + try: + highs.append(float(k[2])) + lows.append(float(k[3])) + except (IndexError, TypeError, ValueError): + continue + if len(highs) < 2: + return None + hi = max(highs) + lo = min(lows) + if hi <= lo: + return None + pos = (cur - lo) / (hi - lo) + return (lo, hi, pos) + + class PositionManager: """仓位管理类""" @@ -424,6 +464,59 @@ class PositionManager: return None except Exception as e: logger.debug(f"{symbol} 趋势入场过滤时出错(忽略,按正常逻辑继续): {e}") + + # ===== 回撤/区间入场过滤(参考近 N 根 K 线的前低/前高,避免买在区间顶部、卖在区间底部)===== + try: + pb_enabled = bool(config.TRADING_CONFIG.get("ENTRY_PULLBACK_FILTER_ENABLED", False)) + if pb_enabled: + min_bars = max(3, int(config.TRADING_CONFIG.get("ENTRY_PULLBACK_MIN_BARS", 5) or 5)) + lookback = max(min_bars, int(config.TRADING_CONFIG.get("ENTRY_PULLBACK_LOOKBACK_BARS", 24) or 24)) + interval_pb = config.TRADING_CONFIG.get("ENTRY_PULLBACK_INTERVAL") or config.TRADING_CONFIG.get( + "ENTRY_INTERVAL", "1h" + ) + max_long_pos = float(config.TRADING_CONFIG.get("ENTRY_PULLBACK_MAX_LONG_IN_RANGE", 0.62) or 0.62) + min_short_pos = float(config.TRADING_CONFIG.get("ENTRY_PULLBACK_MIN_SHORT_IN_RANGE", 0.38) or 0.38) + max_long_pos = min(1.0, max(0.0, max_long_pos)) + min_short_pos = min(1.0, max(0.0, min_short_pos)) + if min_short_pos > max_long_pos: + min_short_pos, max_long_pos = max_long_pos, min_short_pos + + rt = None + try: + rt = self.client.get_realtime_price(symbol) + except Exception: + rt = None + cur_pb = float(rt or estimated_entry_price) + + bars_pb = klines if klines and len(klines) >= min_bars else None + if not bars_pb: + try: + fetched = await self.client.get_klines( + symbol=symbol, interval=str(interval_pb), limit=lookback + ) + bars_pb = fetched if fetched and len(fetched) >= min_bars else None + except Exception as e: + logger.debug(f"{symbol} 回撤过滤拉取K线失败(跳过该过滤): {e}") + bars_pb = None + + metrics = _pullback_range_metrics_from_klines(bars_pb, cur_pb) + if metrics is not None: + lo_pb, hi_pb, pos_pb = metrics + es = estimated_side.upper() + if es == "BUY" and pos_pb > max_long_pos: + logger.info( + f"{symbol} [回撤过滤] 做多跳过:现价={cur_pb:.6f} 在近{len(bars_pb)}根{interval_pb}区间" + f"[{lo_pb:.6f},{hi_pb:.6f}] 中位置={pos_pb:.2f} > 上限{max_long_pos:.2f}(接近区间上沿/前高,易回撤扫损)" + ) + return None + if es == "SELL" and pos_pb < min_short_pos: + logger.info( + f"{symbol} [回撤过滤] 做空跳过:现价={cur_pb:.6f} 在近{len(bars_pb)}根{interval_pb}区间" + f"[{lo_pb:.6f},{hi_pb:.6f}] 中位置={pos_pb:.2f} < 下限{min_short_pos:.2f}(接近区间下沿/前低,易反弹扫损)" + ) + return None + except Exception as e: + logger.debug(f"{symbol} 回撤/区间入场过滤异常(忽略,继续开仓流程): {e}") # 估算止损价格(用于固定风险计算) estimated_stop_loss = None