feat: 一票制并发锁 + MT5硬止损兜底

- 一票制: _pending_long/_pending_short 计数器防同一周期内多信号穿透
- 硬止损: MT5下单时附带sl/tp,倍率1.5x(止损)/1.3x(止盈),比EA软止损更宽
- config.py: 新增 hard_sl_multiplier/hard_tp_multiplier
- 全部 send_order 链路(abc/remote/live/server/dryrun/backtest) 支持 sl/tp 参数
- 对冲模块: 彻底移除
- 日志: 去重+30轮摘要
This commit is contained in:
silencesdg
2026-05-14 13:33:23 +08:00
parent cacca80e11
commit 430376eb61
18 changed files with 1089 additions and 120 deletions
+1
View File
@@ -0,0 +1 @@
162796
+11 -7
View File
@@ -11,7 +11,7 @@ DATA_PROVIDER_MODE = "remote"
# 交易配置
SYMBOL = "XAUUSDz"
INITIAL_CAPITAL = 1944 # 初始资金(2026-05-12 实盘余额 $1944.27
INITIAL_CAPITAL = 2054 # 初始资金(5月12 实盘余额 $2054.23
# 时间配置
TIMEFRAME = 1# M1 (1分钟图) - MT5常量值
@@ -32,7 +32,7 @@ BACKTEST_COUNT = 30000 # 回测数据量
OPTIMIZER_COUNT = 50000 # 优化器数据量
RISK_CONFIG_CONST = {
'enable_time_based_exit': True
'enable_time_based_exit': False # 关闭超时平仓,让止盈/止损/跟踪止损接管
}
# 资金分配配置
@@ -62,8 +62,8 @@ BACKTEST_CONFIG = {
REALTIME_CONFIG = {
"update_interval": 5, # 更新间隔(秒)
"daily_reset_time": "00:00", # 每日重置时间
"max_long_positions": 3, # 最大多头持仓数(增加为3个)
"max_short_positions": 3, # 最大空头持仓数(增加为3个)
"max_long_positions": 1, # 同方向只持一单,避免重复开仓
"max_short_positions": 1, # 同方向只持一单
"min_trade_interval": 0, # 最小交易间隔(分钟),0表示无限制
"enable_auto_trading": True, # 是否启用自动交易
"dry_run": False, # 是否为模拟运行(不实际下单)
@@ -143,13 +143,17 @@ SIGNAL_THRESHOLDS = {
# 风险管理参数(优化器结果 2026-05-10,5万根M1数据)
RISK_CONFIG = {
"stop_loss_pct": -0.046,
"profit_retracement_pct": 0.070,
"min_profit_for_trailing": 0.009,
"profit_retracement_pct": 0.030,
"min_profit_for_trailing": 0.03,
"take_profit_pct": 0.246,
"max_daily_loss": -0.3,
"max_holding_minutes": 133,
"min_profit_for_time_exit": 0.010,
"cooldown_bars": 30
"cooldown_bars": 30,
# ★ 硬止损倍率:MT5 服务器端 SL/TP = 软止损 × 倍率(兜底,仅 EA 挂掉时触发)
# 硬止损必须比软止损宽(倍率>1),否则会抢先触发导致拖尾失效
"hard_sl_multiplier": 1.5, # 硬 SL = 软 SL × 1.5(例如软-4.6%→硬-6.9%
"hard_tp_multiplier": 1.3, # 硬 TP = 软 TP × 1.3(例如软+24.6%→硬+32.0%
}
# 市场状态分析参数
+290
View File
@@ -0,0 +1,290 @@
# MT5 代理服务配置
SERVER_HOST = "0.0.0.0"
SERVER_PORT = 5555
# 远端客户端配置(迁移到其他电脑时填写 MT5 机器的 IP)
REMOTE_SERVER_HOST = "192.168.1.5"
REMOTE_SERVER_PORT = 5555
# 数据提供者模式: "remote" (远程HTTP API) / "local" (本机MT5)
DATA_PROVIDER_MODE = "remote"
# 交易配置
SYMBOL = "XAUUSDz"
INITIAL_CAPITAL = 2054 # 初始资金(5月12日 实盘余额 $2054.23
# 时间配置
TIMEFRAME = 1# M1 (1分钟图) - MT5常量值
# 回测时间范围 (格式: "YYYY-MM-DD")
BACKTEST_START_DATE = "2025-05-01"
BACKTEST_END_DATE = "2025-08-01"
# 优化器时间范围 (格式: "YYYY-MM-DD")
OPTIMIZER_START_DATE = "2025-04-01"
OPTIMIZER_END_DATE = "2025-05-01"
# 安全设置:如果日期获取失败,自动回退到数据量模式
USE_DATE_RANGE = False # 设置为False可强制使用数据量模式
# 兼容性配置 (如果日期配置不可用,则使用数据量)
BACKTEST_COUNT = 30000 # 回测数据量
OPTIMIZER_COUNT = 50000 # 优化器数据量
RISK_CONFIG_CONST = {
'enable_time_based_exit': False # 关闭超时平仓,让止盈/止损/跟踪止损接管
}
# 资金分配配置
CAPITAL_ALLOCATION = {
"long_pct": 0.7, # 多头持仓分配资金比例
"short_pct": 0.3, # 空头持仓分配资金比例
}
# 模拟交易特定配置 (用于dry_run模式)
SIMULATION_CONFIG = {
"leverage": 100, # 模拟杠杆
"contract_size": 1, # XAUUSD的合约大小
"volume_step": 0.01, # 交易手数步长
"volume_min": 0.01, # 最小交易手数
"volume_max": 100.0, # 最大交易手数
"spread": 16, # 点差(点数)
}
# 回测配置
BACKTEST_CONFIG = {
"trade_direction": "both", # 交易方向: "long"(只做多), "short"(只做空), "both"(多空都支持)
"spread": 16, # 点差(点数)
}
# 实时交易配置
REALTIME_CONFIG = {
"update_interval": 5, # 更新间隔(秒)
"daily_reset_time": "00:00", # 每日重置时间
"max_long_positions": 1, # 同方向只持一单,避免重复开仓
"max_short_positions": 1, # 同方向只持一单
"min_trade_interval": 0, # 最小交易间隔(分钟),0表示无限制
"enable_auto_trading": True, # 是否启用自动交易
"dry_run": False, # 是否为模拟运行(不实际下单)
"logging_level": "DEBUG", # 日志级别
"trade_direction": "both", # 交易方向: "long"(只做多), "short"(只做空), "both"(多空都支持)
}
# 对冲配置(信号对冲 + 回撤锁仓)
HEDGE_CONFIG = {
# ── 信号对冲 ──
"signal_hedge_enabled": True, # 启用信号对冲
"signal_hedge_threshold": 2.0, # 加权信号绝对值超此值触发对冲
"signal_hedge_ratio": 0.5, # 对冲手数比例 (0.5=半仓对冲)
"signal_unhedge_threshold": 1.0, # 信号回到此值以下解锁
# ── 回撤锁仓 ──
"drawdown_hedge_enabled": True, # 启用回撤锁仓
"drawdown_hedge_pct": -0.003, # 浮亏超-0.3%触发锁仓
"drawdown_hedge_ratio": 1.0, # 锁仓比例 (1.0=全额锁仓)
# ── 对冲单止盈 ──
"hedge_take_profit_pct": 0.005, # 对冲单自身盈利0.5%止盈
# ── 锁仓管理 ──
"lock_net_profit_pct": 0.0, # 锁仓组合净盈利>0→双平离场
# ── 风控限制 ──
"max_hedges_per_day": 5, # 每日最多对冲5次
}
# 数据获取配置
DATA_CONFIG = {
"m1_bars_count": 5000, # 1分钟K线数据获取数量
}
# 遗传算法优化器配置
GENETIC_OPTIMIZER_CONFIG = {
# 算法参数
"population_size": 50, # 种群大小
"generations": 10, # 进化代数
"crossover_probability": 0.7, # 交叉概率
"mutation_probability": 0.3, # 变异概率
# 选择算法参数
"tournament_size": 3, # 锦标赛选择大小
# 变异算法参数
"mutation_mu": 0, # 变异均值
"mutation_sigma": 0.1, # 变异标准差
"mutation_indpb": 0.1, # 变异概率(每个基因)
# 并行处理
"enable_multiprocessing": True, # 启用多进程
"processes": None, # 进程数,None表示自动检测
# 输出控制
"verbose": True, # 详细输出
"save_generation_info": True, # 保存代数信息
}
'''
此处上面的是固定的参数,可手动调整
--------------------------------------------------------
此处下面所有参数,都将进入优化器进行优化
'''
# 信号阈值配置(优化器结果 2026-05-10,5万根M1数据)
SIGNAL_THRESHOLDS = {
"buy_threshold": 1.344,
"sell_threshold": -2.980
}
# 风险管理参数(优化器结果 2026-05-10,5万根M1数据)
RISK_CONFIG = {
"stop_loss_pct": -0.046,
"profit_retracement_pct": 0.030,
"min_profit_for_trailing": 0.03,
"take_profit_pct": 0.246,
"max_daily_loss": -0.3,
"max_holding_minutes": 133,
"min_profit_for_time_exit": 0.010,
"cooldown_bars": 30
}
# 市场状态分析参数
MARKET_STATE_CONFIG = {
"trend_period": 24,
"retracement_tolerance": 0.425,
"volume_period": 21,
"volume_ma_period": 12
}
# 策略参数配置(优化器结果 2026-05-10,5万根M1数据)
STRATEGY_CONFIG = {
"ma_cross": {
"short_window": 12,
"long_window": 30
},
"rsi": {
"period": 21,
"overbought": 75,
"oversold": 26
},
"bollinger": {
"period": 20,
"std_dev": 2.162
},
"macd": {
"fast_ema": 16,
"slow_ema": 34,
"signal_period": 12
},
"mean_reversion": {
"period": 29,
"std_dev": 2.149
},
"momentum_breakout": {
"period": 15,
"momentum_period": 17
},
"kdj": {
"period": 21
},
"turtle": {
"period": 42
},
"daily_breakout": {
"bars_count": 746
},
"wave_theory": {
"ema_short": 3,
"ema_medium": 16,
"ema_long": 26,
"wave_period": 32,
"range_period": 30,
"adx_period": 23,
"momentum_period": 10,
"range_threshold": 0.002,
"adx_threshold": 23
}
}
# 市场趋势判断权重配置
TREND_INDICATOR_WEIGHTS = {
"price_breakout": -0.0949,
"volume_confirmation": 0.5277,
"momentum oscillator": 0.3677,
"moving_average": 0.1506
}
# 趋势判断阈值
TREND_THRESHOLDS = {
"strong_trend": 0.4182,
"weak_trend": 0.2164,
"volume_spike": 1.5843,
"oversold": 24,
"overbought": 80
}
# 动态权重配置(优化器结果 2026-05-10,5万根M1数据)
DEFAULT_WEIGHTS = {
"ma_cross": 0.809,
"rsi": 1.141,
"bollinger": 0.389,
"mean_reversion": 1.106,
"momentum_breakout": 0.147,
"macd": 1.181,
"kdj": 1.525,
"turtle": 0.559,
"daily_breakout": 1.846,
"wave_theory": 1.361
}
# 市场状态策略权重配置
MARKET_STATE_WEIGHTS = {
"uptrend": {
"ma_cross": 1.50,
"momentum_breakout": 1.20,
"turtle": 0.25,
"macd": 0.35,
"daily_breakout": 1.50,
"rsi": 1.00,
"bollinger": 1.00,
"kdj": 0.40,
"mean_reversion": 0.80,
"wave_theory": 0.20
},
"downtrend": {
"ma_cross": 1.50,
"momentum_breakout": 1.20,
"turtle": 0.25,
"macd": 0.35,
"daily_breakout": 1.50,
"rsi": 1.00,
"bollinger": 1.00,
"kdj": 0.40,
"mean_reversion": 0.80,
"wave_theory": 0.20
},
"ranging": {
"rsi": 1.60,
"bollinger": 1.70,
"mean_reversion": 1.50,
"kdj": 1.00,
"wave_theory": 0.50,
"ma_cross": 0.70,
"macd": 0.20,
"turtle": 0.10,
"momentum_breakout": 0.50,
"daily_breakout": 0.90
},
"none": DEFAULT_WEIGHTS
}
# 市场趋势置信度阈值配置
CONFIDENCE_THRESHOLDS = {
"high_confidence": 0.8474,
"medium_confidence": 0.4964
}
+290
View File
@@ -0,0 +1,290 @@
# MT5 代理服务配置
SERVER_HOST = "0.0.0.0"
SERVER_PORT = 5555
# 远端客户端配置(迁移到其他电脑时填写 MT5 机器的 IP)
REMOTE_SERVER_HOST = "192.168.1.5"
REMOTE_SERVER_PORT = 5555
# 数据提供者模式: "remote" (远程HTTP API) / "local" (本机MT5)
DATA_PROVIDER_MODE = "remote"
# 交易配置
SYMBOL = "XAUUSDz"
INITIAL_CAPITAL = 2054 # 初始资金(5月12日 实盘余额 $2054.23
# 时间配置
TIMEFRAME = 1# M1 (1分钟图) - MT5常量值
# 回测时间范围 (格式: "YYYY-MM-DD")
BACKTEST_START_DATE = "2025-05-01"
BACKTEST_END_DATE = "2025-08-01"
# 优化器时间范围 (格式: "YYYY-MM-DD")
OPTIMIZER_START_DATE = "2025-04-01"
OPTIMIZER_END_DATE = "2025-05-01"
# 安全设置:如果日期获取失败,自动回退到数据量模式
USE_DATE_RANGE = False # 设置为False可强制使用数据量模式
# 兼容性配置 (如果日期配置不可用,则使用数据量)
BACKTEST_COUNT = 30000 # 回测数据量
OPTIMIZER_COUNT = 50000 # 优化器数据量
RISK_CONFIG_CONST = {
'enable_time_based_exit': False # 关闭超时平仓,让止盈/止损/跟踪止损接管
}
# 资金分配配置
CAPITAL_ALLOCATION = {
"long_pct": 0.7, # 多头持仓分配资金比例
"short_pct": 0.3, # 空头持仓分配资金比例
}
# 模拟交易特定配置 (用于dry_run模式)
SIMULATION_CONFIG = {
"leverage": 100, # 模拟杠杆
"contract_size": 1, # XAUUSD的合约大小
"volume_step": 0.01, # 交易手数步长
"volume_min": 0.01, # 最小交易手数
"volume_max": 100.0, # 最大交易手数
"spread": 16, # 点差(点数)
}
# 回测配置
BACKTEST_CONFIG = {
"trade_direction": "both", # 交易方向: "long"(只做多), "short"(只做空), "both"(多空都支持)
"spread": 16, # 点差(点数)
}
# 实时交易配置
REALTIME_CONFIG = {
"update_interval": 5, # 更新间隔(秒)
"daily_reset_time": "00:00", # 每日重置时间
"max_long_positions": 1, # 同方向只持一单,避免重复开仓
"max_short_positions": 1, # 同方向只持一单
"min_trade_interval": 0, # 最小交易间隔(分钟),0表示无限制
"enable_auto_trading": True, # 是否启用自动交易
"dry_run": False, # 是否为模拟运行(不实际下单)
"logging_level": "DEBUG", # 日志级别
"trade_direction": "both", # 交易方向: "long"(只做多), "short"(只做空), "both"(多空都支持)
}
# 对冲配置(信号对冲 + 回撤锁仓)
HEDGE_CONFIG = {
# ── 信号对冲 ──
"signal_hedge_enabled": True, # 启用信号对冲
"signal_hedge_threshold": 2.0, # 加权信号绝对值超此值触发对冲
"signal_hedge_ratio": 0.5, # 对冲手数比例 (0.5=半仓对冲)
"signal_unhedge_threshold": 1.0, # 信号回到此值以下解锁
# ── 回撤锁仓 ──
"drawdown_hedge_enabled": True, # 启用回撤锁仓
"drawdown_hedge_pct": -0.003, # 浮亏超-0.3%触发锁仓
"drawdown_hedge_ratio": 1.0, # 锁仓比例 (1.0=全额锁仓)
# ── 对冲单止盈 ──
"hedge_take_profit_pct": 0.005, # 对冲单自身盈利0.5%止盈
# ── 锁仓管理 ──
"lock_net_profit_pct": 0.0, # 锁仓组合净盈利>0→双平离场
# ── 风控限制 ──
"max_hedges_per_day": 5, # 每日最多对冲5次
}
# 数据获取配置
DATA_CONFIG = {
"m1_bars_count": 5000, # 1分钟K线数据获取数量
}
# 遗传算法优化器配置
GENETIC_OPTIMIZER_CONFIG = {
# 算法参数
"population_size": 50, # 种群大小
"generations": 10, # 进化代数
"crossover_probability": 0.7, # 交叉概率
"mutation_probability": 0.3, # 变异概率
# 选择算法参数
"tournament_size": 3, # 锦标赛选择大小
# 变异算法参数
"mutation_mu": 0, # 变异均值
"mutation_sigma": 0.1, # 变异标准差
"mutation_indpb": 0.1, # 变异概率(每个基因)
# 并行处理
"enable_multiprocessing": True, # 启用多进程
"processes": None, # 进程数,None表示自动检测
# 输出控制
"verbose": True, # 详细输出
"save_generation_info": True, # 保存代数信息
}
'''
此处上面的是固定的参数,可手动调整
--------------------------------------------------------
此处下面所有参数,都将进入优化器进行优化
'''
# 信号阈值配置(优化器结果 2026-05-10,5万根M1数据)
SIGNAL_THRESHOLDS = {
"buy_threshold": 1.344,
"sell_threshold": -2.980
}
# 风险管理参数(优化器结果 2026-05-10,5万根M1数据)
RISK_CONFIG = {
"stop_loss_pct": -0.046,
"profit_retracement_pct": 0.030,
"min_profit_for_trailing": 0.03,
"take_profit_pct": 0.246,
"max_daily_loss": -0.3,
"max_holding_minutes": 133,
"min_profit_for_time_exit": 0.010,
"cooldown_bars": 30
}
# 市场状态分析参数
MARKET_STATE_CONFIG = {
"trend_period": 24,
"retracement_tolerance": 0.425,
"volume_period": 21,
"volume_ma_period": 12
}
# 策略参数配置(优化器结果 2026-05-10,5万根M1数据)
STRATEGY_CONFIG = {
"ma_cross": {
"short_window": 12,
"long_window": 30
},
"rsi": {
"period": 21,
"overbought": 75,
"oversold": 26
},
"bollinger": {
"period": 20,
"std_dev": 2.162
},
"macd": {
"fast_ema": 16,
"slow_ema": 34,
"signal_period": 12
},
"mean_reversion": {
"period": 29,
"std_dev": 2.149
},
"momentum_breakout": {
"period": 15,
"momentum_period": 17
},
"kdj": {
"period": 21
},
"turtle": {
"period": 42
},
"daily_breakout": {
"bars_count": 746
},
"wave_theory": {
"ema_short": 3,
"ema_medium": 16,
"ema_long": 26,
"wave_period": 32,
"range_period": 30,
"adx_period": 23,
"momentum_period": 10,
"range_threshold": 0.002,
"adx_threshold": 23
}
}
# 市场趋势判断权重配置
TREND_INDICATOR_WEIGHTS = {
"price_breakout": -0.0949,
"volume_confirmation": 0.5277,
"momentum oscillator": 0.3677,
"moving_average": 0.1506
}
# 趋势判断阈值
TREND_THRESHOLDS = {
"strong_trend": 0.4182,
"weak_trend": 0.2164,
"volume_spike": 1.5843,
"oversold": 24,
"overbought": 80
}
# 动态权重配置(优化器结果 2026-05-10,5万根M1数据)
DEFAULT_WEIGHTS = {
"ma_cross": 0.809,
"rsi": 1.141,
"bollinger": 0.389,
"mean_reversion": 1.106,
"momentum_breakout": 0.147,
"macd": 1.181,
"kdj": 1.525,
"turtle": 0.559,
"daily_breakout": 1.846,
"wave_theory": 1.361
}
# 市场状态策略权重配置
MARKET_STATE_WEIGHTS = {
"uptrend": {
"ma_cross": 1.50,
"momentum_breakout": 1.20,
"turtle": 0.25,
"macd": 0.35,
"daily_breakout": 1.50,
"rsi": 1.00,
"bollinger": 1.00,
"kdj": 0.40,
"mean_reversion": 0.80,
"wave_theory": 0.20
},
"downtrend": {
"ma_cross": 1.50,
"momentum_breakout": 1.20,
"turtle": 0.25,
"macd": 0.35,
"daily_breakout": 1.50,
"rsi": 1.00,
"bollinger": 1.00,
"kdj": 0.40,
"mean_reversion": 0.80,
"wave_theory": 0.20
},
"ranging": {
"rsi": 1.60,
"bollinger": 1.70,
"mean_reversion": 1.50,
"kdj": 1.00,
"wave_theory": 0.50,
"ma_cross": 0.70,
"macd": 0.20,
"turtle": 0.10,
"momentum_breakout": 0.50,
"daily_breakout": 0.90
},
"none": DEFAULT_WEIGHTS
}
# 市场趋势置信度阈值配置
CONFIDENCE_THRESHOLDS = {
"high_confidence": 0.8474,
"medium_confidence": 0.4964
}
+1 -1
View File
@@ -37,7 +37,7 @@ class DataProvider(ABC):
"""获取品种信息(合约规格等)"""
@abstractmethod
def send_order(self, symbol, order_type, volume):
def send_order(self, symbol, order_type, volume, sl=None, tp=None):
"""发送订单"""
@abstractmethod
+1 -1
View File
@@ -64,7 +64,7 @@ class BacktestDataProvider(DataProvider):
symbol, timeframe, count, self.current_index
)
def send_order(self, symbol, order_type, volume):
def send_order(self, symbol, order_type, volume, sl=None, tp=None):
self.simulated_ticket_counter += 1
logger.info(f"[回测模式] 下单: {order_type} {volume:.2f}{symbol}")
return {'order': self.simulated_ticket_counter}
+1 -1
View File
@@ -48,7 +48,7 @@ class DryRunDataProvider(DataProvider):
def get_positions(self, symbol):
return []
def send_order(self, symbol, order_type, volume):
def send_order(self, symbol, order_type, volume, sl=None, tp=None):
self.simulated_ticket_counter += 1
price_data = self.get_current_price(symbol)
price = price_data['last'] if price_data else "N/A"
+3 -1
View File
@@ -53,7 +53,7 @@ class LiveDataProvider(DataProvider):
def get_symbol_info(self, symbol):
return _get_mt5().symbol_info(symbol)
def send_order(self, symbol, order_type, volume):
def send_order(self, symbol, order_type, volume, sl=None, tp=None):
price_data = self.get_current_price(symbol)
if not price_data:
logger.error(f"无法获取 {symbol} 价格,无法下单")
@@ -92,6 +92,8 @@ class LiveDataProvider(DataProvider):
"volume": volume,
"type": order_type_mt5,
"price": price,
"sl": sl or 0.0, # ★ MT5 硬止损(0=不设)
"tp": tp or 0.0, # ★ MT5 硬止盈
"deviation": 20,
"magic": 234000,
"comment": f"{order_type} order",
+7 -2
View File
@@ -92,11 +92,16 @@ class RemoteDataProvider(DataProvider):
except Exception:
return None
def send_order(self, symbol, order_type, volume):
def send_order(self, symbol, order_type, volume, sl=None, tp=None):
try:
body = {"symbol": symbol, "order_type": order_type, "volume": volume}
if sl is not None:
body["sl"] = sl
if tp is not None:
body["tp"] = tp
resp = self._session.post(
f"{self.base_url}/order",
json={"symbol": symbol, "order_type": order_type, "volume": volume},
json=body,
timeout=10,
)
if resp.status_code != 200:
-3
View File
@@ -19,9 +19,6 @@ class RiskController:
def monitor_positions(self, current_price, dry_run=False, weighted_signal=0.0):
self.position_manager.monitor_positions(current_price, dry_run, weighted_signal)
# 对冲摘要日志
if self.position_manager.hedge_manager and self.position_manager.hedge_manager.active_hedges > 0:
logger.info(f"🔒 活跃对冲: {self.position_manager.hedge_manager.active_hedges}")
def sync_state(self):
self.position_manager.update_equity()
+100 -42
View File
@@ -8,7 +8,6 @@ from config import (
RISK_CONFIG_CONST, SIMULATION_CONFIG
)
from core.risk.exit_rules import ExitRuleEngine, ExitContext
from core.risk.hedge import HedgeManager
class PositionManager:
@@ -39,6 +38,10 @@ class PositionManager:
self.enable_time_based_exit = RISK_CONFIG_CONST.get("enable_time_based_exit", False)
self.max_daily_loss = risk.get("max_daily_loss", -0.30)
# ★ 硬止损倍率(MT5 服务器端兜底)
self.hard_sl_mult = risk.get("hard_sl_multiplier", 1.5)
self.hard_tp_mult = risk.get("hard_tp_multiplier", 1.3)
# 资金管理
self.initial_capital = INITIAL_CAPITAL
self.long_capital_pct = CAPITAL_ALLOCATION.get("long_pct", 0.5)
@@ -53,6 +56,9 @@ class PositionManager:
self.cooldown_bars = risk.get("cooldown_bars", 30)
self._cooldown_counter = 0
# 拒单去重:避免连续刷相同拒绝日志
self._last_rejected_msg = None
# 退出规则引擎
exit_config = {
"stop_loss_pct": self.stop_loss_pct,
@@ -73,7 +79,12 @@ class PositionManager:
# 对冲管理器
from config import HEDGE_CONFIG
self.hedge_manager = HedgeManager(self, HEDGE_CONFIG)
self.hedge_manager = None # 对冲模块已禁用
# ★ 一票制并发锁:防止同一周期内多个信号穿透
# 开仓前+1,完成后-1,持仓检查时累加 pending 计数
self._pending_long = 0
self._pending_short = 0
# ── 仓位计算 ──
@@ -103,6 +114,18 @@ class PositionManager:
volume = max(min_volume, min(volume, max_volume))
return volume
# ── 去重日志 ──
def _reject_log(self, msg):
"""只在首次出现或拒绝原因变化时打印"""
if msg != self._last_rejected_msg:
logger.info(msg)
self._last_rejected_msg = msg
def _clear_reject_log(self):
"""开仓成功/平仓后重置去重状态"""
self._last_rejected_msg = None
# ── 开仓 ──
def open_position(self, direction, current_price, signal_strength=0.0, dry_run=False):
@@ -123,65 +146,99 @@ class PositionManager:
# 交易方向限制
if self.trade_direction == "long" and direction == "sell":
logger.info("当前配置只允许做多,忽略卖出信号")
self._reject_log("当前配置只允许做多,忽略卖出信号")
return False
elif self.trade_direction == "short" and direction == "buy":
logger.info("当前配置只允许做空,忽略买入信号")
self._reject_log("当前配置只允许做空,忽略买入信号")
return False
# ★ 每日亏损检查(幽灵代码落地)
current_time = current_price.get('time', pd.Timestamp.now())
if self._check_max_daily_loss(current_time):
logger.warning("当日亏损已达上限,禁止开新仓")
self._reject_log("当日亏损已达上限,禁止开新仓")
return False
# 最大持仓数检查 — 优先使用 risk_config 传入值,回退到 REALTIME_CONFIG
# ★ 加入 pending 计数器防并发穿透:同一周期内多信号同时检查时,第一个开仓后
# 其 pending 计数会让后续信号看到正确数量,不会误开
max_key = f'max_{position_type}_positions'
max_positions = self._risk_config.get(max_key, None)
if max_positions is None:
from config import REALTIME_CONFIG
max_positions = REALTIME_CONFIG.get(max_key, 1)
current_count = len([p for p in self.positions if p['position_type'] == position_type])
pending_count = self._pending_long if position_type == 'long' else self._pending_short
current_count = len([p for p in self.positions if p['position_type'] == position_type]) + pending_count
if 0 < max_positions <= current_count:
logger.info(f"已达到最大{position_type}持仓({max_positions}),忽略信号")
self._reject_log(f"已达{position_type}最大持仓({max_positions}),忽略信号")
return False
# 资金分配
capital_pct = self.long_capital_pct if direction == 'buy' else self.short_capital_pct
capital_for_trade = self.total_equity * capital_pct
position_volume = self._calculate_position_size(capital_for_trade, {'last': execution_price})
if position_volume <= 0:
logger.info("仓位大小为0,无法开仓")
return False
order_result = self.data_provider.send_order(self.symbol, direction, position_volume)
if order_result is None:
logger.error("订单返回None")
return False
# ★ 占位:标记一个 pending 订单,防止并发穿透
if position_type == 'long':
self._pending_long += 1
else:
self._pending_short += 1
try:
order_id = order_result['order'] if isinstance(order_result, dict) else order_result.order
except Exception as e:
logger.error(f"解析订单ID失败: {e}")
return False
# 资金分配
capital_pct = self.long_capital_pct if direction == 'buy' else self.short_capital_pct
capital_for_trade = self.total_equity * capital_pct
if order_result and order_id > 0:
new_position = {
'ticket': order_id,
'symbol': self.symbol,
'entry_price': execution_price,
'entry_time': current_time,
'position_type': position_type,
'quantity': position_volume,
'peak_profit_pct': 0.0,
}
self.positions.append(new_position)
logger.info(f"开仓成功: {direction} @ {execution_price:.2f}, 手数: {position_volume:.2f}, Ticket: {order_id}")
return True
else:
logger.error(f"开仓失败: {direction} @ {execution_price:.2f}")
return False
position_volume = self._calculate_position_size(capital_for_trade, {'last': execution_price})
if position_volume <= 0:
self._reject_log("仓位大小为0,无法开仓")
return False
# ★ 计算 MT5 硬止损/硬止盈(兜底安全网)
hard_sl_price = None
hard_tp_price = None
if direction == 'buy':
if self.hard_sl_mult > 0:
hard_sl_price = round(execution_price * (1 + self.stop_loss_pct * self.hard_sl_mult), 2)
if self.hard_tp_mult > 0:
hard_tp_price = round(execution_price * (1 + self.take_profit_pct * self.hard_tp_mult), 2)
else:
if self.hard_sl_mult > 0:
hard_sl_price = round(execution_price * (1 - self.stop_loss_pct * self.hard_sl_mult), 2)
if self.hard_tp_mult > 0:
hard_tp_price = round(execution_price * (1 - self.take_profit_pct * self.hard_tp_mult), 2)
order_result = self.data_provider.send_order(
self.symbol, direction, position_volume,
sl=hard_sl_price, tp=hard_tp_price
)
if order_result is None:
logger.error("订单返回None")
return False
try:
order_id = order_result['order'] if isinstance(order_result, dict) else order_result.order
except Exception as e:
logger.error(f"解析订单ID失败: {e}")
return False
if order_result and order_id > 0:
new_position = {
'ticket': order_id,
'symbol': self.symbol,
'entry_price': execution_price,
'entry_time': current_time,
'position_type': position_type,
'quantity': position_volume,
'peak_profit_pct': 0.0,
}
self.positions.append(new_position)
self._clear_reject_log()
logger.info(f"开仓成功: {direction} @ {execution_price:.2f}, 手数: {position_volume:.2f}, Ticket: {order_id}")
return True
else:
logger.error(f"开仓失败: {direction} @ {execution_price:.2f}")
return False
finally:
# ★ 释放 pending 占位(无论成功/失败/异常)
if position_type == 'long':
self._pending_long -= 1
else:
self._pending_short -= 1
# ── 持仓监控 ──
@@ -233,6 +290,7 @@ class PositionManager:
if positions_to_remove:
self.positions = [p for p in self.positions if p not in positions_to_remove]
self._cooldown_counter = self.cooldown_bars
self._clear_reject_log()
self.update_equity()
self.cleanup_peak_data()
@@ -353,8 +411,8 @@ class PositionManager:
'peak_profit_pct': restored_peak
}
self.positions.append(new_position)
logger.info(f"同步持仓 {pos.ticket}: 恢复峰值={restored_peak:.6%}")
logger.info(f"持仓已从MT5同步: {len(self.positions)}")
if live_positions:
logger.debug(f"MT5同步: {len(self.positions)}持仓")
self.update_equity()
# ── 交易摘要 ──
+48 -8
View File
@@ -32,10 +32,12 @@ from config import (
SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE,
USE_DATE_RANGE, INITIAL_CAPITAL, SIGNAL_THRESHOLDS, DEFAULT_WEIGHTS,
RISK_CONFIG, GENETIC_OPTIMIZER_CONFIG,
MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS
MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS,
DATA_PROVIDER_MODE, REMOTE_SERVER_HOST, REMOTE_SERVER_PORT
)
from utils.constants import PERIOD_H1
from core.utils import get_rates, initialize, shutdown
from core.data.remote import RemoteDataProvider
from logger import logger
# 导入策略模块确保 StrategyRegistry 已注册
@@ -46,6 +48,18 @@ _multi_tf: MultiTimeframeDataStore | None = None
_cached_signals: pd.DataFrame | None = None
_registry: StrategyRegistry | None = None
# MT5 结构化数组 dtype(用于远程 API JSON → numpy 转换)
_MT5_RATES_DTYPE = np.dtype([
('time', 'i8'),
('open', 'f8'),
('high', 'f8'),
('low', 'f8'),
('close', 'f8'),
('tick_volume', 'i8'),
('spread', 'i4'),
('real_volume', 'i8'),
])
def init_worker():
"""多进程worker初始化:抑制日志噪音"""
@@ -397,15 +411,41 @@ def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
return (total_pnl,)
def _json_to_mt5_rates(rates_data):
"""将远程API JSON rates 转换为 MT5 兼容的 numpy 结构化数组"""
if not rates_data:
return None
records = []
for r in rates_data:
records.append((
r['time'], r['open'], r['high'], r['low'], r['close'],
r.get('tick_volume', 0), r.get('spread', 0), r.get('real_volume', 0),
))
return np.array(records, dtype=_MT5_RATES_DTYPE)
def load_historical_data():
"""一次性加载M1数据并返回 MultiTimeframeDataStore"""
initialize()
rates = (
get_rates(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE)
if USE_DATE_RANGE else
get_rates(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT)
)
shutdown()
if DATA_PROVIDER_MODE == "remote":
provider = RemoteDataProvider(host=REMOTE_SERVER_HOST, port=REMOTE_SERVER_PORT)
if not provider.initialize():
raise RuntimeError("远程MT5 API初始化失败,请检查 Windows MT5 是否运行")
rates_json = provider.get_historical_data(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT)
provider.shutdown()
if not rates_json:
raise RuntimeError("远程获取历史数据失败")
rates = _json_to_mt5_rates(rates_json)
else:
initialize()
rates = (
get_rates(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE)
if USE_DATE_RANGE else
get_rates(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT)
)
shutdown()
if rates is None or len(rates) == 0:
raise RuntimeError("获取历史数据失败")
+39 -51
View File
@@ -3,7 +3,7 @@ import signal
import sys
from datetime import datetime
from logger import logger
from config import SYMBOL, TIMEFRAME, REALTIME_CONFIG, SIGNAL_THRESHOLDS
from config import SYMBOL, TIMEFRAME, REALTIME_CONFIG, SIGNAL_THRESHOLDS, RISK_CONFIG, RISK_CONFIG_CONST
from core.risk import RiskController
from execution.weights import DynamicWeightManager
@@ -16,6 +16,7 @@ class RealtimeTrader:
self.running = False
self.risk_controller = None
self.weight_manager = None
self._cycle_count = 0
def _initialize(self):
if not self.data_provider.initialize():
@@ -23,25 +24,38 @@ class RealtimeTrader:
self.risk_controller = RiskController(self.data_provider)
self.weight_manager = DynamicWeightManager(self.data_provider)
self.risk_controller.sync_state()
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
logger.info("实时交易系统初始化完成")
# ★ 启动参数一览
logger.info("=" * 50)
logger.info(f"品种: {SYMBOL} | 周期: M{TIMEFRAME} | 间隔: {self.update_interval}s")
logger.info(f"风控: 止损={RISK_CONFIG['stop_loss_pct']:.1%} | "
f"止盈={RISK_CONFIG['take_profit_pct']:.1%} | "
f"拖尾激活={RISK_CONFIG['min_profit_for_trailing']:.1%} | "
f"拖尾回撤={RISK_CONFIG['profit_retracement_pct']:.1%}")
logger.info(f"信号: 买入阈值={SIGNAL_THRESHOLDS.get('buy_threshold',1.5)} | "
f"卖出阈值={SIGNAL_THRESHOLDS.get('sell_threshold',-1.5)}")
logger.info(f"仓位: 最多多={REALTIME_CONFIG['max_long_positions']} 最多空={REALTIME_CONFIG['max_short_positions']} | "
f"超时平仓={'' if RISK_CONFIG_CONST.get('enable_time_based_exit',True) else ''}")
logger.info(f"对冲: 信号对冲={'' if REALTIME_CONFIG.get('hedge_enabled',False) else ''} | "
f"锁仓={'' if REALTIME_CONFIG.get('lock_enabled',False) else ''}")
logger.info("=" * 50)
return True
def _signal_handler(self, signum, frame):
logger.info(f"接收信号 {signum},准备退出...")
logger.info(f"接收信号 {signum},准备退出...")
self.stop()
def _run_cycle(self):
try:
self._cycle_count += 1
self.risk_controller.sync_state()
current_price = self.data_provider.get_current_price(SYMBOL)
if not current_price:
logger.warning("无法获取当前价格,跳过本次循环")
return
strategies_with_weights = self.weight_manager.get_current_strategies_and_weights()
@@ -51,66 +65,38 @@ class RealtimeTrader:
for strat, weight in strategies_with_weights:
signals.append(strat.generate_signal())
weights.append(weight)
# 打印详细信号日志
logger.info("--- 信号计算详情 ---")
for i, (strat, weight) in enumerate(strategies_with_weights):
signal = signals[i]
weighted_signal = signal * weight
strat_name = strat.name
logger.info(f" 策略: {strat_name:<25} | 信号: {signal:6.2f} | 权重: {weight:6.2f} | 加权信号: {weighted_signal:6.2f}")
logger.info("--------------------")
weighted_signal_sum = sum(s * w for s, w in zip(signals, weights))
buy_threshold = SIGNAL_THRESHOLDS.get('buy_threshold', 1.5)
sell_threshold = SIGNAL_THRESHOLDS.get('sell_threshold', -1.5)
logger.info(f"加权信号: {weighted_signal_sum:.2f} (买入阈值: {buy_threshold}, 卖出阈值: {sell_threshold})")
# logger.info(f"信号比较: {weighted_signal_sum} > {buy_threshold} = {weighted_signal_sum > buy_threshold}")
# logger.info(f"信号比较: {weighted_signal_sum} < {sell_threshold} = {weighted_signal_sum < sell_threshold}")
direction = None
if weighted_signal_sum > buy_threshold:
direction = "buy"
elif weighted_signal_sum < sell_threshold:
direction = "sell"
# 只在信号触发时打印决策依据
if direction:
logger.info(f"准备执行{direction}交易,信号强度: {weighted_signal_sum:.2f}")
success = self.risk_controller.process_trading_signal(direction, current_price, weighted_signal_sum)
if not success:
logger.warning(f"{direction}交易执行失败")
else:
logger.info(f"{direction}交易执行成功")
logger.info(f"⚡ 信号触发 | 加权={weighted_signal_sum:.2f} | "
f"阈值=[{sell_threshold:.2f}, {buy_threshold:.2f}] | "
f"方向={direction.upper()} | 价格={current_price['last']:.2f}")
self.risk_controller.process_trading_signal(direction, current_price, weighted_signal_sum)
self.risk_controller.monitor_positions(current_price, weighted_signal=weighted_signal_sum)
# --- 状态汇总日志 ---
logger.info("--- 财务状况更新 ---")
open_positions = self.risk_controller.position_manager.positions
if not open_positions:
logger.info(" 当前无持仓")
else:
logger.info(f" 当前持仓: {len(open_positions)} ")
for pos in open_positions:
pnl_pct = self.risk_controller.position_manager._calculate_pnl_pct(pos, current_price['last'])
# 计算持仓时间
holding_time = current_price['time'] - pos['entry_time']
holding_minutes = holding_time.total_seconds() / 60
logger.info(f" - Ticket {pos['ticket']}: {pos['position_type']} {pos['symbol']} @ {pos['entry_price']:.2f} | 持仓时间: {holding_minutes:.1f}分钟 | 浮动盈亏: {pnl_pct:.2%}")
trade_summary = self.risk_controller.position_manager.get_trade_summary()
if trade_summary and trade_summary['total_trades'] > 0:
logger.info(" 已平仓交易摘要:")
logger.info(f" - 总交易: {trade_summary['total_trades']}, 盈利: {trade_summary['winning_trades']}, 亏损: {trade_summary['losing_trades']}, 胜率: {trade_summary['win_rate']:.2f}%")
logger.info(f" - 总净盈亏: ${trade_summary['total_profit_loss']:.2f}")
logger.info(f" 总权益: ${self.risk_controller.position_manager.total_equity:.2f}")
logger.info("----------------------")
# 每30个周期打印一次状态摘要
if self._cycle_count % 30 == 0:
pm = self.risk_controller.position_manager
n = len(pm.positions)
summary = pm.get_trade_summary()
logger.info(f"📊 周期#{self._cycle_count} | 持仓={n} | "
f"净值=${pm.total_equity:.2f} | "
f"已平{summary['total_trades']}笔 胜率{summary['win_rate']:.0f}% 净${summary['total_profit_loss']:+.2f}")
except Exception as e:
import traceback
logger.error(f"交易周期执行失败: {e}\n{traceback.format_exc()}")
logger.error(f"交易周期失败: {e}\n{traceback.format_exc()}")
def start(self):
if not self._initialize(): return
@@ -132,10 +118,12 @@ class RealtimeTrader:
if self.risk_controller:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.risk_controller.save_trade_history(f"realtime_trades_{timestamp}")
summary = self.risk_controller.position_manager.get_trade_summary()
if summary['total_trades'] > 0:
logger.info(f"📊 本次运行: {summary['total_trades']}笔 胜率{summary['win_rate']:.0f}% 净${summary['total_profit_loss']:+.2f}")
except Exception as e:
logger.error(f"保存交易记录失败: {e}")
finally:
self.data_provider.shutdown()
logger.info("实时交易系统已停止")
sys.exit(0)
+6
View File
@@ -51,6 +51,12 @@ def main():
trader = RealtimeTrader(data_provider, update_interval=REALTIME_CONFIG['update_interval'])
print("\n正在启动交易系统... (按 Ctrl+C 可安全停止)")
# 写入 PID 文件供重启脚本使用
pid_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.ea_pid')
with open(pid_file, 'w') as f:
f.write(str(os.getpid()))
trader.start()
+4 -1
View File
@@ -68,6 +68,8 @@ class OrderRequest(BaseModel):
symbol: str
order_type: str
volume: float
sl: float = None # ★ 硬止损价格
tp: float = None # ★ 硬止盈价格
class CloseRequest(BaseModel):
ticket: int
@@ -146,7 +148,8 @@ def api_symbol(symbol: str):
def api_order(req: OrderRequest):
_ensure_connected()
with _lock:
result = _provider.send_order(req.symbol, req.order_type, req.volume)
result = _provider.send_order(req.symbol, req.order_type, req.volume,
sl=req.sl, tp=req.tp)
if result is None:
return JSONResponse(status_code=503, content={"error": "下单失败"})
return serialize_order_result(result)
+255
View File
@@ -0,0 +1,255 @@
#!/usr/bin/env python3
"""每日自动优化 — 遗传算法跑参数 → 写入config → 重启实盘EA
通过 cron 调用: python3 scripts/daily_optimize.py
"""
import sys, os, json, re, time, signal, shutil
from datetime import datetime
from pathlib import Path
PROJECT_DIR = Path(__file__).resolve().parent.parent
os.chdir(str(PROJECT_DIR))
sys.path.insert(0, str(PROJECT_DIR))
from logger import setup_logger
setup_logger("INFO")
from logger import logger
CONFIG_PATH = PROJECT_DIR / "config.py"
BACKUP_DIR = PROJECT_DIR / "config_backups"
RESTART_SIGNAL = PROJECT_DIR / ".restart_signal"
def run_optimizer():
"""运行遗传算法优化,返回 best_params dict"""
from execution.optimize import run_optimizer as _run
logger.info("🧬 开始遗传算法优化...")
best_params, fitness = _run()
logger.info(f"✅ 优化完成 适应度={fitness:.2f}")
return best_params
def backup_config():
"""备份当前 config.py"""
BACKUP_DIR.mkdir(exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
dst = BACKUP_DIR / f"config_{ts}.py"
shutil.copy(CONFIG_PATH, dst)
logger.info(f"📦 已备份配置: {dst}")
def update_config(best_params: dict):
"""将优化结果写回 config.py"""
content = CONFIG_PATH.read_text(encoding="utf-8")
# ═══ RISK_CONFIG ═══
risk_map = {
"stop_loss_pct": "stop_loss_pct",
"profit_retracement_pct": "profit_retracement_pct",
"min_profit_for_trailing": "min_profit_for_trailing",
"take_profit_pct": "take_profit_pct",
"max_holding_minutes": "max_holding_minutes",
"min_profit_for_time_exit": "min_profit_for_time_exit",
}
for opt_key, cfg_key in risk_map.items():
if opt_key in best_params:
val = best_params[opt_key]
content = re.sub(
rf'("{cfg_key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content
)
# ═══ SIGNAL_THRESHOLDS ═══
if "buy_threshold" in best_params:
content = re.sub(
r'("buy_threshold":\s*)[\d.\-e]+',
rf'\g<1>{best_params["buy_threshold"]}',
content
)
if "sell_threshold" in best_params:
content = re.sub(
r'("sell_threshold":\s*)[\d.\-e]+',
rf'\g<1>{best_params["sell_threshold"]}',
content
)
# ═══ MARKET_STATE_CONFIG ═══
market_map = {
"market_trend_period": "trend_period",
"market_retracement_tolerance": "retracement_tolerance",
"market_volume_period": "volume_period",
"market_volume_ma_period": "volume_ma_period",
}
for opt_key, cfg_key in market_map.items():
if opt_key in best_params:
val = int(best_params[opt_key]) if "period" in opt_key else best_params[opt_key]
content = re.sub(
rf'("{cfg_key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content
)
# ═══ STRATEGY_CONFIG (strategy params) ═══
strategy_param_map = {
# MACrossStrategy
"ma_cross_short_window": ("ma_cross", "short_window"),
"ma_cross_long_window": ("ma_cross", "long_window"),
# RSIStrategy
"rsi_period": ("rsi", "period"),
"rsi_overbought": ("rsi", "overbought"),
"rsi_oversold": ("rsi", "oversold"),
# BollingerStrategy
"bollinger_period": ("bollinger", "period"),
"bollinger_std_dev": ("bollinger", "std_dev"),
# MACDStrategy
"macd_fast_ema": ("macd", "fast_ema"),
"macd_slow_ema": ("macd", "slow_ema"),
"macd_signal_period": ("macd", "signal_period"),
# MeanReversionStrategy
"mean_reversion_period": ("mean_reversion", "period"),
"mean_reversion_std_dev": ("mean_reversion", "std_dev"),
# MomentumBreakoutStrategy
"momentum_breakout_period": ("momentum_breakout", "period"),
"momentum_breakout_momentum_period": ("momentum_breakout", "momentum_period"),
# KDJStrategy
"kdj_period": ("kdj", "period"),
# TurtleStrategy
"turtle_period": ("turtle", "period"),
# DailyBreakoutStrategy
"daily_breakout_bars_count": ("daily_breakout", "bars_count"),
# WaveTheoryStrategy
"wave_ema_short": ("wave_theory", "ema_short"),
"wave_ema_medium": ("wave_theory", "ema_medium"),
"wave_ema_long": ("wave_theory", "ema_long"),
"wave_period": ("wave_theory", "wave_period"),
"wave_range_period": ("wave_theory", "range_period"),
"wave_adx_period": ("wave_theory", "adx_period"),
"wave_momentum_period": ("wave_theory", "momentum_period"),
"wave_range_threshold": ("wave_theory", "range_threshold"),
"wave_adx_threshold": ("wave_theory", "adx_threshold"),
}
for opt_key, (section, key) in strategy_param_map.items():
if opt_key in best_params:
val = best_params[opt_key]
if isinstance(val, float) and abs(val - round(val)) < 1e-6:
val = int(round(val))
content = re.sub(
rf'("{key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content,
count=1,
)
# ═══ DEFAULT_WEIGHTS ═══
weight_map = {
"weight_MACrossStrategy": "ma_cross",
"weight_RSIStrategy": "rsi",
"weight_BollingerStrategy": "bollinger",
"weight_MeanReversionStrategy": "mean_reversion",
"weight_MomentumBreakoutStrategy": "momentum_breakout",
"weight_MACDStrategy": "macd",
"weight_KDJStrategy": "kdj",
"weight_TurtleStrategy": "turtle",
"weight_DailyBreakoutStrategy": "daily_breakout",
"weight_WaveTheoryStrategy": "wave_theory",
}
for opt_key, cfg_key in weight_map.items():
if opt_key in best_params:
val = best_params[opt_key]
content = re.sub(
rf'("{cfg_key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content
)
# ═══ TREND_INDICATOR_WEIGHTS ═══
trend_weight_map = {
"trend_price_breakout_weight": "price_breakout",
"trend_volume_confirmation_weight": "volume_confirmation",
"trend_momentum_oscillator_weight": "momentum_oscillator",
"trend_moving_average_weight": "moving_average",
}
for opt_key, cfg_key in trend_weight_map.items():
if opt_key in best_params:
val = best_params[opt_key]
content = re.sub(
rf'("{cfg_key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content
)
# ═══ TREND_THRESHOLDS ═══
trend_thresh_map = {
"trend_strong_threshold": "strong_trend",
"trend_weak_threshold": "weak_trend",
"trend_volume_spike": "volume_spike",
"trend_oversold": "oversold",
"trend_overbought": "overbought",
}
for opt_key, cfg_key in trend_thresh_map.items():
if opt_key in best_params:
val = best_params[opt_key]
if "sold" in opt_key or "bought" in opt_key:
val = int(round(val))
content = re.sub(
rf'("{cfg_key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content
)
# ═══ CONFIDENCE_THRESHOLDS ═══
conf_map = {
"confidence_high": "high_confidence",
"confidence_medium": "medium_confidence",
}
for opt_key, cfg_key in conf_map.items():
if opt_key in best_params:
val = best_params[opt_key]
content = re.sub(
rf'("{cfg_key}":\s*)[\d.\-e]+',
rf'\g<1>{val}',
content
)
CONFIG_PATH.write_text(content, encoding="utf-8")
logger.info("✏️ 配置已更新")
def restart_ea():
"""通过信号文件通知 restart_ea.sh 重启"""
RESTART_SIGNAL.write_text(datetime.now().isoformat())
logger.info("📡 已发送重启信号")
def main():
logger.info("=" * 60)
logger.info("📅 每日自动优化启动")
logger.info(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info("=" * 60)
# 1. 备份当前配置
backup_config()
# 2. 运行优化
try:
best_params = run_optimizer()
except Exception as e:
logger.error(f"优化失败: {e}")
import traceback
traceback.print_exc()
return 1
# 3. 写入 config.py
update_config(best_params)
# 4. 触发重启
restart_ea()
logger.info("✅ 每日优化流程完成")
return 0
if __name__ == "__main__":
sys.exit(main())
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
# 重启 EA:杀旧进程 → 应用新配置 → 启动新进程
# 由 daily_optimize.py 或 cron 调用
set -e
PROJECT_DIR="/home/songkl/mt5_python_ea_suite"
cd "$PROJECT_DIR"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 重启 EA..."
# 1. 杀掉所有 realtime.py 进程
pkill -f "python.*run/realtime.py" 2>/dev/null || true
sleep 2
# 强制清理残留
pkill -9 -f "python.*run/realtime.py" 2>/dev/null || true
sleep 1
# 2. 清空旧交易记录(新配置从零开始)
rm -f realtime_trades_*.json realtime_trades_*.csv
# 3. 清空日志
> logs/strategy.log
# 4. 启动新 EA
nohup python3 run/realtime.py >> /dev/null 2>&1 &
NEW_PID=$!
echo $NEW_PID > .ea_pid
echo "[$(date '+%Y-%m-%d %H:%M:%S')] EA 已重启 PID=$NEW_PID"
+2 -2
View File
@@ -37,14 +37,14 @@ class MACrossStrategy(BaseStrategy):
is_cross_up = latest['short_ma'] > latest['long_ma'] and previous['short_ma'] <= previous['long_ma']
logger.debug(f"金叉判断 (is_cross_up): {is_cross_up}")
if is_cross_up:
logger.info(f"{self.name}: 检测到金叉,生成买入信号")
logger.debug(f"{self.name}: 检测到金叉,生成买入信号")
return 1
# 判断死叉
is_cross_down = latest['short_ma'] < latest['long_ma'] and previous['short_ma'] >= previous['long_ma']
logger.debug(f"死叉判断 (is_cross_down): {is_cross_down}")
if is_cross_down:
logger.info(f"{self.name}: 检测到死叉,生成卖出信号")
logger.debug(f"{self.name}: 检测到死叉,生成卖出信号")
return -1
logger.debug(f"--- {self.name} 信号生成结束 (无信号) ---")