This commit is contained in:
薇薇安 2026-01-29 23:34:15 +08:00
parent 53396adf26
commit 9490207537
6 changed files with 143 additions and 23 deletions

View File

@ -402,7 +402,19 @@ async def get_global_configs(
"value": 8, "value": 8,
"type": "number", "type": "number",
"category": "scan", "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": { "MIN_VOLUME_24H_STRICT": {
"value": 10000000, "value": 10000000,
@ -428,6 +440,24 @@ async def get_global_configs(
"category": "strategy", "category": "strategy",
"description": "智能入场开关。关闭后回归纯限价单模式", "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(): for k, meta in ADDITIONAL_STRATEGY_DEFAULTS.items():
if k not in result: if k not in result:

View File

@ -744,6 +744,7 @@ class ConfigManager:
'TRAILING_STOP_PROTECT', 'TRAILING_STOP_PROTECT',
'MIN_VOLATILITY', 'MIN_VOLATILITY',
'TAKE_PROFIT_PERCENT', 'TAKE_PROFIT_PERCENT',
'TAKE_PROFIT_1_PERCENT', # 分步止盈第一目标默认15%
'STOP_LOSS_PERCENT', 'STOP_LOSS_PERCENT',
'MIN_STOP_LOSS_PRICE_PCT', 'MIN_STOP_LOSS_PRICE_PCT',
'MIN_TAKE_PROFIT_PRICE_PCT', 'MIN_TAKE_PROFIT_PRICE_PCT',
@ -807,7 +808,8 @@ class ConfigManager:
# - 提高ATR倍数从1.5到2.0),给市场波动更多空间 # - 提高ATR倍数从1.5到2.0),给市场波动更多空间
# - 提高最小价格变动百分比从2%到2.5%),避免止损过紧 # - 提高最小价格变动百分比从2%到2.5%),避免止损过紧
'STOP_LOSS_PERCENT': eff_get('STOP_LOSS_PERCENT', 0.12), # 默认12%(保证金百分比) '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_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过小时计算出不切实际的微小止盈距离 '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动态止损 'USE_ATR_STOP_LOSS': eff_get('USE_ATR_STOP_LOSS', True), # 是否使用ATR动态止损
@ -825,7 +827,8 @@ class ConfigManager:
# 市场扫描30分钟主周期 # 市场扫描30分钟主周期
'SCAN_INTERVAL': eff_get('SCAN_INTERVAL', scan_interval_default), # 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% 'MAX_SCAN_SYMBOLS': eff_get('MAX_SCAN_SYMBOLS', 250), # 扫描的最大交易对数量增加到250提升覆盖率到46%
'EXCLUDE_MAJOR_COINS': eff_get('EXCLUDE_MAJOR_COINS', True), # 是否排除主流币BTC、ETH、BNB等专注于山寨币 'EXCLUDE_MAJOR_COINS': eff_get('EXCLUDE_MAJOR_COINS', True), # 是否排除主流币BTC、ETH、BNB等专注于山寨币
'KLINE_INTERVAL': eff_get('KLINE_INTERVAL', '1h'), 'KLINE_INTERVAL': eff_get('KLINE_INTERVAL', '1h'),
@ -889,6 +892,11 @@ class ConfigManager:
# 当前交易预设(让 trading_system 能知道是哪种模式) # 当前交易预设(让 trading_system 能知道是哪种模式)
'TRADING_PROFILE': profile, '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): def _sync_to_redis(self):

View File

@ -199,11 +199,13 @@ def _get_trading_config():
'MIN_POSITION_PERCENT': 0.01, # 最小仓位1% 'MIN_POSITION_PERCENT': 0.01, # 最小仓位1%
'MIN_MARGIN_USDT': 5.0, # 最小保证金5美元 'MIN_MARGIN_USDT': 5.0, # 最小保证金5美元
'MIN_CHANGE_PERCENT': 0.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% 'MAX_SCAN_SYMBOLS': 250, # 扫描前250个增加覆盖率从27.6%提升到46.0%
'EXCLUDE_MAJOR_COINS': True, # 排除主流币BTC、ETH、BNB等专注于山寨币 'EXCLUDE_MAJOR_COINS': True, # 排除主流币BTC、ETH、BNB等专注于山寨币
'STOP_LOSS_PERCENT': 0.12, # 止损12%(保证金百分比) '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_STOP_LOSS_PRICE_PCT': 0.025, # 最小止损价格变动2.5%2026-01-29优化从2%提高到2.5%,给波动更多空间)
'MIN_TAKE_PROFIT_PRICE_PCT': 0.02, # 最小止盈价格变动2% 'MIN_TAKE_PROFIT_PRICE_PCT': 0.02, # 最小止盈价格变动2%
'USE_ATR_STOP_LOSS': True, # 使用ATR动态止损 'USE_ATR_STOP_LOSS': True, # 使用ATR动态止损
@ -284,6 +286,11 @@ def _get_trading_config():
'SMART_ENTRY_ENABLED': True, # 开启智能入场,提高成交率 'SMART_ENTRY_ENABLED': True, # 开启智能入场,提高成交率
'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值≥82026-01-29优化与MIN_SIGNAL_STRENGTH保持一致 'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值≥82026-01-29优化与MIN_SIGNAL_STRENGTH保持一致
'ENTRY_SYMBOL_COOLDOWN_SEC': 1800, # 同一币种冷却30分钟1800秒快速验证模式缩短冷却以增加交易频率 '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_TIMEOUT_SEC': 180, # 智能入场总预算(秒)(限价/追价逻辑内部使用)
'ENTRY_STEP_WAIT_SEC': 15, # 每步等待成交时间(秒) 'ENTRY_STEP_WAIT_SEC': 15, # 每步等待成交时间(秒)
'ENTRY_CHASE_MAX_STEPS': 4, # 最多追价步数(逐步减少 offset 'ENTRY_CHASE_MAX_STEPS': 4, # 最多追价步数(逐步减少 offset

View File

@ -161,7 +161,11 @@ class MarketScanner:
reverse=True 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 self.top_symbols = top_n

View File

@ -589,20 +589,19 @@ class PositionManager:
margin_usdt = None margin_usdt = None
# 分步止盈(基于"实际成交价 + 已计算的止损/止盈" # 分步止盈(基于"实际成交价 + 已计算的止损/止盈"
# ⚠️ 关键修复:第一目标应该使用 TAKE_PROFIT_PERCENT10%固定止盈而不是盈亏比1:1 # ⚠️ 第一目标使用 TAKE_PROFIT_1_PERCENT默认15%),与第二目标 TAKE_PROFIT_PERCENT 分离,提高整体盈亏比
# 这样第一目标更容易触发保证拿到10%盈利然后剩余50%追求更高收益 # 第一目标和触发条件必须一致,都使用 TAKE_PROFIT_1_PERCENT
# 第一目标和触发条件必须一致,都使用 TAKE_PROFIT_PERCENT take_profit_1_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15)
take_profit_1_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10)
# 兼容百分比形式和比例形式 # 兼容百分比形式和比例形式
if take_profit_1_pct_margin is not None and take_profit_1_pct_margin > 1: 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 take_profit_1_pct_margin = take_profit_1_pct_margin / 100.0
# 计算第一目标止盈价(基于保证金10% # 计算第一目标止盈价(基于保证金默认15%
if margin_usdt and margin_usdt > 0 and quantity > 0: if margin_usdt and margin_usdt > 0 and quantity > 0:
take_profit_1_amount = margin_usdt * take_profit_1_pct_margin take_profit_1_amount = margin_usdt * take_profit_1_pct_margin
if side == 'BUY': 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: else:
take_profit_1 = entry_price - (take_profit_1_amount / quantity) # 10%固定止盈 take_profit_1 = entry_price - (take_profit_1_amount / quantity) # 第一目标止盈默认15%
else: else:
# 如果无法计算保证金回退到盈亏比1:1 # 如果无法计算保证金回退到盈亏比1:1
if side == 'BUY': if side == 'BUY':
@ -1713,12 +1712,11 @@ class PositionManager:
partial_profit_taken = position_info.get('partialProfitTaken', False) partial_profit_taken = position_info.get('partialProfitTaken', False)
remaining_quantity = position_info.get('remainingQuantity', quantity) 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: if not partial_profit_taken and take_profit_1 is not None:
# ⚠️ 关键修复:直接使用配置的 TAKE_PROFIT_PERCENT而不是从止盈价格反推 # 直接使用配置的 TAKE_PROFIT_1_PERCENT与开仓时计算的第一目标一致
# 因为第一目标现在已经是基于 TAKE_PROFIT_PERCENT 计算的,直接使用配置值更准确 take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15)
take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10)
# 兼容百分比形式和比例形式 # 兼容百分比形式和比例形式
if take_profit_1_pct_margin_config > 1: if take_profit_1_pct_margin_config > 1:
take_profit_1_pct_margin_config = take_profit_1_pct_margin_config / 100.0 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 take_profit_2 = position_info.get('takeProfit2', position_info.get('takeProfit')) # 第二目标1.5:1
# ⚠️ 注意partial_profit_taken和remaining_quantity已在方法开头初始化这里不需要重复定义 # ⚠️ 注意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: if not partial_profit_taken and take_profit_1 is not None:
# ⚠️ 关键修复:直接使用配置的 TAKE_PROFIT_PERCENT而不是从止盈价格反推 # 直接使用配置的 TAKE_PROFIT_1_PERCENT与开仓时计算的第一目标一致
# 因为第一目标现在已经是基于 TAKE_PROFIT_PERCENT 计算的,直接使用配置值更准确 take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15)
take_profit_1_pct_margin_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10)
# 兼容百分比形式和比例形式 # 兼容百分比形式和比例形式
if take_profit_1_pct_margin_config > 1: if take_profit_1_pct_margin_config > 1:
take_profit_1_pct_margin_config = take_profit_1_pct_margin_config / 100.0 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: if pnl_percent_margin >= take_profit_1_pct_margin:
# ⚠️ 2026-01-27优化动态读取配置值更新日志文案 take_profit_pct_config = config.TRADING_CONFIG.get('TAKE_PROFIT_1_PERCENT', 0.15)
take_profit_pct_config = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.10)
if take_profit_pct_config > 1: if take_profit_pct_config > 1:
take_profit_pct_config = take_profit_pct_config / 100.0 take_profit_pct_config = take_profit_pct_config / 100.0
take_profit_pct_display = take_profit_pct_config * 100 take_profit_pct_display = take_profit_pct_config * 100

View File

@ -3,6 +3,7 @@
""" """
import logging import logging
import os import os
import time
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from typing import Dict, List, Optional from typing import Dict, List, Optional
try: try:
@ -558,8 +559,82 @@ class RiskManager:
logger.info(f"{symbol} 今日开仓次数已达上限:{c}/{max_daily},跳过") logger.info(f"{symbol} 今日开仓次数已达上限:{c}/{max_daily},跳过")
return False 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 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: def _daily_entries_key(self) -> str:
try: try:
aid = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1) aid = int(os.getenv("ATS_ACCOUNT_ID") or os.getenv("ACCOUNT_ID") or 1)