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, # 强信号阈值≥8(2026-01-29优化:与MIN_SIGNAL_STRENGTH保持一致) 'SMART_ENTRY_STRONG_SIGNAL': 8, # 强信号阈值≥8(2026-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_PERCENT(10%固定止盈),而不是盈亏比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)