From 949020753756b2801a7a6fa8e41fcd8211bf2af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Thu, 29 Jan 2026 23:34:15 +0800 Subject: [PATCH] a --- backend/api/routes/config.py | 32 ++++++++++++- backend/config_manager.py | 12 ++++- trading_system/config.py | 11 ++++- trading_system/market_scanner.py | 6 ++- trading_system/position_manager.py | 30 ++++++------ trading_system/risk_manager.py | 75 ++++++++++++++++++++++++++++++ 6 files changed, 143 insertions(+), 23 deletions(-) diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index c483f3d..9cb78c6 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -402,7 +402,19 @@ async def get_global_configs( "value": 8, "type": "number", "category": "scan", - "description": "每次扫描后处理的交易对数量", + "description": "每次扫描后优先处理的交易对数量", + }, + "SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT": { + "value": 8, + "type": "number", + "category": "scan", + "description": "智能补单:多返回的候选数量。当前 TOP_N 中部分因冷却等被跳过时,仍会尝试这批额外候选,避免无单可下。", + }, + "TAKE_PROFIT_1_PERCENT": { + "value": 0.15, + "type": "number", + "category": "strategy", + "description": "分步止盈第一目标(保证金百分比,如 0.15=15%)。第一目标触发后了结50%仓位,剩余追求第二目标。", }, "MIN_VOLUME_24H_STRICT": { "value": 10000000, @@ -428,6 +440,24 @@ async def get_global_configs( "category": "strategy", "description": "智能入场开关。关闭后回归纯限价单模式", }, + "SYMBOL_LOSS_COOLDOWN_ENABLED": { + "value": True, + "type": "boolean", + "category": "strategy", + "description": "是否启用同一交易对连续亏损后的冷却(避免连续亏损后继续交易)。2026-01-29新增。", + }, + "SYMBOL_MAX_CONSECUTIVE_LOSSES": { + "value": 2, + "type": "number", + "category": "strategy", + "description": "最大允许连续亏损次数(超过则禁止交易该交易对一段时间)。2026-01-29新增。", + }, + "SYMBOL_LOSS_COOLDOWN_SEC": { + "value": 3600, + "type": "number", + "category": "strategy", + "description": "连续亏损后的冷却时间(秒),默认1小时。2026-01-29新增。", + }, } for k, meta in ADDITIONAL_STRATEGY_DEFAULTS.items(): if k not in result: diff --git a/backend/config_manager.py b/backend/config_manager.py index ce4dd52..ee6f7ae 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -744,6 +744,7 @@ class ConfigManager: 'TRAILING_STOP_PROTECT', 'MIN_VOLATILITY', 'TAKE_PROFIT_PERCENT', + 'TAKE_PROFIT_1_PERCENT', # 分步止盈第一目标(默认15%) 'STOP_LOSS_PERCENT', 'MIN_STOP_LOSS_PRICE_PCT', 'MIN_TAKE_PROFIT_PRICE_PCT', @@ -807,7 +808,8 @@ class ConfigManager: # - 提高ATR倍数(从1.5到2.0),给市场波动更多空间 # - 提高最小价格变动百分比(从2%到2.5%),避免止损过紧 'STOP_LOSS_PERCENT': eff_get('STOP_LOSS_PERCENT', 0.12), # 默认12%(保证金百分比) - 'TAKE_PROFIT_PERCENT': eff_get('TAKE_PROFIT_PERCENT', 0.10), # 默认10%(2026-01-27优化:进一步降低止盈目标,更容易触发,提升止盈单比例) + 'TAKE_PROFIT_PERCENT': eff_get('TAKE_PROFIT_PERCENT', 0.10), # 默认10%(第二目标/单目标止盈) + 'TAKE_PROFIT_1_PERCENT': eff_get('TAKE_PROFIT_1_PERCENT', 0.15), # 默认15%(分步止盈第一目标,提高整体盈亏比) 'MIN_STOP_LOSS_PRICE_PCT': eff_get('MIN_STOP_LOSS_PRICE_PCT', 0.025), # 默认2.5%(2026-01-29优化:从2%提高到2.5%,给波动更多空间) '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动态止损 @@ -825,7 +827,8 @@ class ConfigManager: # 市场扫描(30分钟主周期) 'SCAN_INTERVAL': eff_get('SCAN_INTERVAL', scan_interval_default), # 30分钟(增加交易机会) - 'TOP_N_SYMBOLS': eff_get('TOP_N_SYMBOLS', 8), # 每次扫描后处理的交易对数量(增加到8,给更多选择余地) + 'TOP_N_SYMBOLS': eff_get('TOP_N_SYMBOLS', 8), # 每次扫描后优先处理的交易对数量 + 'SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT': eff_get('SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT', 8), # 智能补单:多返回的候选数量,冷却时仍可尝试后续交易对 'MAX_SCAN_SYMBOLS': eff_get('MAX_SCAN_SYMBOLS', 250), # 扫描的最大交易对数量(增加到250,提升覆盖率到46%) 'EXCLUDE_MAJOR_COINS': eff_get('EXCLUDE_MAJOR_COINS', True), # 是否排除主流币(BTC、ETH、BNB等),专注于山寨币 'KLINE_INTERVAL': eff_get('KLINE_INTERVAL', '1h'), @@ -889,6 +892,11 @@ class ConfigManager: # 当前交易预设(让 trading_system 能知道是哪种模式) 'TRADING_PROFILE': profile, + # ⚠️ 2026-01-29新增:同一交易对连续亏损过滤(避免连续亏损后继续交易) + 'SYMBOL_LOSS_COOLDOWN_ENABLED': eff_get('SYMBOL_LOSS_COOLDOWN_ENABLED', True), + 'SYMBOL_MAX_CONSECUTIVE_LOSSES': eff_get('SYMBOL_MAX_CONSECUTIVE_LOSSES', 2), + 'SYMBOL_LOSS_COOLDOWN_SEC': eff_get('SYMBOL_LOSS_COOLDOWN_SEC', 3600), + } def _sync_to_redis(self): diff --git a/trading_system/config.py b/trading_system/config.py index 1c746e7..ea9c067 100644 --- a/trading_system/config.py +++ b/trading_system/config.py @@ -199,11 +199,13 @@ def _get_trading_config(): 'MIN_POSITION_PERCENT': 0.01, # 最小仓位1% 'MIN_MARGIN_USDT': 5.0, # 最小保证金5美元 'MIN_CHANGE_PERCENT': 0.5, # 最小价格变动0.5% - 'TOP_N_SYMBOLS': 8, # 选择信号最强的8个(给更多选择余地,避免错过好机会) + 'TOP_N_SYMBOLS': 8, # 选择信号最强的8个优先处理 + 'SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT': 8, # 智能补单:多返回8个候选,冷却时仍可尝试后续交易对 'MAX_SCAN_SYMBOLS': 250, # 扫描前250个(增加覆盖率,从27.6%提升到46.0%) 'EXCLUDE_MAJOR_COINS': True, # 排除主流币(BTC、ETH、BNB等),专注于山寨币 'STOP_LOSS_PERCENT': 0.12, # 止损12%(保证金百分比) - 'TAKE_PROFIT_PERCENT': 0.10, # 止盈10%(2026-01-27优化:进一步降低止盈目标,更容易触发,提升止盈单比例) + 'TAKE_PROFIT_PERCENT': 0.10, # 第二目标/单目标止盈10% + 'TAKE_PROFIT_1_PERCENT': 0.15, # 分步止盈第一目标15%,提高整体盈亏比 'MIN_STOP_LOSS_PRICE_PCT': 0.025, # 最小止损价格变动2.5%(2026-01-29优化:从2%提高到2.5%,给波动更多空间) 'MIN_TAKE_PROFIT_PRICE_PCT': 0.02, # 最小止盈价格变动2% 'USE_ATR_STOP_LOSS': True, # 使用ATR动态止损 @@ -284,6 +286,11 @@ def _get_trading_config(): 'SMART_ENTRY_ENABLED': True, # 开启智能入场,提高成交率 'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值≥8(2026-01-29优化:与MIN_SIGNAL_STRENGTH保持一致) 'ENTRY_SYMBOL_COOLDOWN_SEC': 1800, # 同一币种冷却30分钟(1800秒),快速验证模式:缩短冷却以增加交易频率 + + # ===== 同一交易对连续亏损过滤(避免连续亏损后继续交易)===== + 'SYMBOL_LOSS_COOLDOWN_ENABLED': True, # 是否启用同一交易对连续亏损后的冷却 + 'SYMBOL_MAX_CONSECUTIVE_LOSSES': 2, # 最大允许连续亏损次数(超过则禁止交易) + 'SYMBOL_LOSS_COOLDOWN_SEC': 3600, # 连续亏损后的冷却时间(秒),默认1小时 'ENTRY_TIMEOUT_SEC': 180, # 智能入场总预算(秒)(限价/追价逻辑内部使用) 'ENTRY_STEP_WAIT_SEC': 15, # 每步等待成交时间(秒) 'ENTRY_CHASE_MAX_STEPS': 4, # 最多追价步数(逐步减少 offset) diff --git a/trading_system/market_scanner.py b/trading_system/market_scanner.py index 17a3982..ce10057 100644 --- a/trading_system/market_scanner.py +++ b/trading_system/market_scanner.py @@ -161,7 +161,11 @@ class MarketScanner: reverse=True ) - top_n = sorted_results[:cfg.get('TOP_N_SYMBOLS', config.TRADING_CONFIG['TOP_N_SYMBOLS'])] + # 智能补单:返回 TOP_N + 额外候选数,当前 TOP_N 中部分因冷却等被跳过时,策略仍会尝试后续交易对,避免无单可下 + top_n_val = cfg.get('TOP_N_SYMBOLS', config.TRADING_CONFIG['TOP_N_SYMBOLS']) + extra = cfg.get('SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT', config.TRADING_CONFIG.get('SCAN_EXTRA_SYMBOLS_FOR_SUPPLEMENT', 8)) + take_count = min(len(sorted_results), top_n_val + extra) + top_n = sorted_results[:take_count] self.top_symbols = top_n diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 57e67fd..1bc63b6 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -589,20 +589,19 @@ class PositionManager: margin_usdt = None # 分步止盈(基于"实际成交价 + 已计算的止损/止盈") - # ⚠️ 关键修复:第一目标应该使用 TAKE_PROFIT_PERCENT(10%固定止盈),而不是盈亏比1:1 - # 这样第一目标更容易触发,保证拿到10%盈利,然后剩余50%追求更高收益 - # 第一目标和触发条件必须一致,都使用 TAKE_PROFIT_PERCENT - take_profit_1_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10) + # ⚠️ 第一目标使用 TAKE_PROFIT_1_PERCENT(默认15%),与第二目标 TAKE_PROFIT_PERCENT 分离,提高整体盈亏比 + # 第一目标和触发条件必须一致,都使用 TAKE_PROFIT_1_PERCENT + take_profit_1_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15) # 兼容百分比形式和比例形式 if take_profit_1_pct_margin is not None and take_profit_1_pct_margin > 1: take_profit_1_pct_margin = take_profit_1_pct_margin / 100.0 - # 计算第一目标止盈价(基于保证金10%) + # 计算第一目标止盈价(基于保证金,默认15%) if margin_usdt and margin_usdt > 0 and quantity > 0: take_profit_1_amount = margin_usdt * take_profit_1_pct_margin if side == 'BUY': - take_profit_1 = entry_price + (take_profit_1_amount / quantity) # 10%固定止盈 + take_profit_1 = entry_price + (take_profit_1_amount / quantity) # 第一目标止盈(默认15%) else: - take_profit_1 = entry_price - (take_profit_1_amount / quantity) # 10%固定止盈 + take_profit_1 = entry_price - (take_profit_1_amount / quantity) # 第一目标止盈(默认15%) else: # 如果无法计算保证金,回退到盈亏比1:1 if side == 'BUY': @@ -1713,12 +1712,11 @@ class PositionManager: partial_profit_taken = position_info.get('partialProfitTaken', False) remaining_quantity = position_info.get('remainingQuantity', quantity) - # 第一目标:10%固定止盈(基于保证金),了结50%仓位,保证拿到10%盈利 + # 第一目标:TAKE_PROFIT_1_PERCENT 止盈(默认15%保证金),了结50%仓位 # ✅ 已移除时间锁限制,可以立即执行 if not partial_profit_taken and take_profit_1 is not None: - # ⚠️ 关键修复:直接使用配置的 TAKE_PROFIT_PERCENT,而不是从止盈价格反推 - # 因为第一目标现在已经是基于 TAKE_PROFIT_PERCENT 计算的,直接使用配置值更准确 - take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10) + # 直接使用配置的 TAKE_PROFIT_1_PERCENT,与开仓时计算的第一目标一致 + take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15) # 兼容百分比形式和比例形式 if take_profit_1_pct_margin_config > 1: take_profit_1_pct_margin_config = take_profit_1_pct_margin_config / 100.0 @@ -3010,11 +3008,10 @@ class PositionManager: take_profit_2 = position_info.get('takeProfit2', position_info.get('takeProfit')) # 第二目标(1.5:1) # ⚠️ 注意:partial_profit_taken和remaining_quantity已在方法开头初始化,这里不需要重复定义 - # 第一目标:10%固定止盈(基于保证金),了结50%仓位,保证拿到10%盈利 + # 第一目标:TAKE_PROFIT_1_PERCENT 止盈(默认15%保证金),了结50%仓位 if not partial_profit_taken and take_profit_1 is not None: - # ⚠️ 关键修复:直接使用配置的 TAKE_PROFIT_PERCENT,而不是从止盈价格反推 - # 因为第一目标现在已经是基于 TAKE_PROFIT_PERCENT 计算的,直接使用配置值更准确 - take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10) + # 直接使用配置的 TAKE_PROFIT_1_PERCENT,与开仓时计算的第一目标一致 + take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15) # 兼容百分比形式和比例形式 if take_profit_1_pct_margin_config > 1: take_profit_1_pct_margin_config = take_profit_1_pct_margin_config / 100.0 @@ -3022,8 +3019,7 @@ class PositionManager: # 直接比较当前盈亏百分比与第一目标(基于保证金,使用配置值) if pnl_percent_margin >= take_profit_1_pct_margin: - # ⚠️ 2026-01-27优化:动态读取配置值,更新日志文案 - take_profit_pct_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10) + take_profit_pct_config = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15) if take_profit_pct_config > 1: take_profit_pct_config = take_profit_pct_config / 100.0 take_profit_pct_display = take_profit_pct_config * 100 diff --git a/trading_system/risk_manager.py b/trading_system/risk_manager.py index 72d2588..6adeb1e 100644 --- a/trading_system/risk_manager.py +++ b/trading_system/risk_manager.py @@ -3,6 +3,7 @@ """ import logging import os +import time from datetime import datetime, timezone, timedelta from typing import Dict, List, Optional try: @@ -558,8 +559,82 @@ class RiskManager: logger.info(f"{symbol} 今日开仓次数已达上限:{c}/{max_daily},跳过") return False + # ⚠️ 2026-01-29新增:检查同一交易对连续亏损情况,避免连续亏损后继续交易 + try: + loss_cooldown_enabled = bool(config.TRADING_CONFIG.get("SYMBOL_LOSS_COOLDOWN_ENABLED", True)) + if loss_cooldown_enabled: + max_consecutive_losses = int(config.TRADING_CONFIG.get("SYMBOL_MAX_CONSECUTIVE_LOSSES", 2) or 2) + loss_cooldown_sec = int(config.TRADING_CONFIG.get("SYMBOL_LOSS_COOLDOWN_SEC", 3600) or 3600) # 默认1小时 + + # 查询该交易对最近的交易记录(仅查询已平仓的) + recent_losses = await self._check_recent_losses(symbol, max_consecutive_losses, loss_cooldown_sec) + if recent_losses >= max_consecutive_losses: + logger.info( + f"{symbol} [连续亏损过滤] 最近{max_consecutive_losses}次交易连续亏损," + f"禁止交易{loss_cooldown_sec}秒(冷却中)" + ) + return False + except Exception as e: + logger.debug(f"{symbol} 检查连续亏损时出错(忽略,允许交易): {e}") + return True + async def _check_recent_losses(self, symbol: str, max_consecutive: int, cooldown_sec: int) -> int: + """ + 检查同一交易对最近的连续亏损次数 + + Args: + symbol: 交易对 + max_consecutive: 最大允许连续亏损次数 + cooldown_sec: 冷却时间(秒) + + Returns: + 连续亏损次数(如果>=max_consecutive,则应该禁止交易) + """ + try: + # 尝试从数据库查询最近的交易记录 + from database.models import Trade + + # 查询最近N+1次已平仓的交易(多查一次,确保能判断是否连续) + recent_trades = Trade.get_all( + symbol=symbol, + status='closed', + account_id=int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1) + ) + + # 按平仓时间倒序排序(最新的在前) + recent_trades = sorted( + recent_trades, + key=lambda x: (x.get('exit_time') or x.get('entry_time') or 0), + reverse=True + )[:max_consecutive + 1] # 只取最近N+1次 + + if not recent_trades: + return 0 + + # 检查是否在冷却时间内 + now = int(time.time()) + latest_trade = recent_trades[0] + latest_exit_time = latest_trade.get('exit_time') or latest_trade.get('entry_time') or 0 + + # 如果最新交易还在冷却时间内,检查连续亏损 + if now - latest_exit_time < cooldown_sec: + consecutive_losses = 0 + for trade in recent_trades: + pnl = float(trade.get('pnl', 0) or 0) + if pnl < 0: # 亏损 + consecutive_losses += 1 + else: # 盈利,中断连续亏损 + break + return consecutive_losses + + # 如果最新交易已超过冷却时间,不限制 + return 0 + + except Exception as e: + logger.debug(f"查询{symbol}最近交易记录失败(忽略,允许交易): {e}") + return 0 + def _daily_entries_key(self) -> str: try: aid = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)