From 430376eb6190338620ed1a4f996a54aa49c270a0 Mon Sep 17 00:00:00 2001 From: silencesdg Date: Thu, 14 May 2026 13:33:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=80=E7=A5=A8=E5=88=B6=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E9=94=81=20+=20MT5=E7=A1=AC=E6=AD=A2=E6=8D=9F?= =?UTF-8?q?=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 一票制: _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轮摘要 --- .ea_pid | 1 + config.py | 18 +- config_backups/config_20260514_101933.py | 290 +++++++++++++++++++++++ config_backups/config_20260514_102336.py | 290 +++++++++++++++++++++++ core/data/abc.py | 2 +- core/data/backtest.py | 2 +- core/data/dryrun.py | 2 +- core/data/live.py | 4 +- core/data/remote.py | 9 +- core/risk/controller.py | 3 - core/risk/position.py | 142 +++++++---- execution/optimize.py | 56 ++++- execution/realtime_trader.py | 90 +++---- run/realtime.py | 6 + run/server.py | 5 +- scripts/daily_optimize.py | 255 ++++++++++++++++++++ scripts/restart_ea.sh | 30 +++ strategies/ma_cross.py | 4 +- 18 files changed, 1089 insertions(+), 120 deletions(-) create mode 100644 .ea_pid create mode 100644 config_backups/config_20260514_101933.py create mode 100644 config_backups/config_20260514_102336.py create mode 100755 scripts/daily_optimize.py create mode 100755 scripts/restart_ea.sh diff --git a/.ea_pid b/.ea_pid new file mode 100644 index 0000000..f94e213 --- /dev/null +++ b/.ea_pid @@ -0,0 +1 @@ +162796 \ No newline at end of file diff --git a/config.py b/config.py index a00aa33..bdf7f55 100644 --- a/config.py +++ b/config.py @@ -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%) } # 市场状态分析参数 diff --git a/config_backups/config_20260514_101933.py b/config_backups/config_20260514_101933.py new file mode 100644 index 0000000..533d2b9 --- /dev/null +++ b/config_backups/config_20260514_101933.py @@ -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 +} diff --git a/config_backups/config_20260514_102336.py b/config_backups/config_20260514_102336.py new file mode 100644 index 0000000..533d2b9 --- /dev/null +++ b/config_backups/config_20260514_102336.py @@ -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 +} diff --git a/core/data/abc.py b/core/data/abc.py index 5ff4464..79c6498 100644 --- a/core/data/abc.py +++ b/core/data/abc.py @@ -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 diff --git a/core/data/backtest.py b/core/data/backtest.py index 6611b1d..d4e1c52 100644 --- a/core/data/backtest.py +++ b/core/data/backtest.py @@ -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} diff --git a/core/data/dryrun.py b/core/data/dryrun.py index a56aeb7..f454245 100644 --- a/core/data/dryrun.py +++ b/core/data/dryrun.py @@ -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" diff --git a/core/data/live.py b/core/data/live.py index cfc6fa3..9801cad 100644 --- a/core/data/live.py +++ b/core/data/live.py @@ -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", diff --git a/core/data/remote.py b/core/data/remote.py index a2e5104..a3205ef 100644 --- a/core/data/remote.py +++ b/core/data/remote.py @@ -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: diff --git a/core/risk/controller.py b/core/risk/controller.py index 4f99705..4827eb3 100644 --- a/core/risk/controller.py +++ b/core/risk/controller.py @@ -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() diff --git a/core/risk/position.py b/core/risk/position.py index 87b12ae..b1a4256 100644 --- a/core/risk/position.py +++ b/core/risk/position.py @@ -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() # ── 交易摘要 ── diff --git a/execution/optimize.py b/execution/optimize.py index 6b194d3..2b3f66f 100644 --- a/execution/optimize.py +++ b/execution/optimize.py @@ -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("获取历史数据失败") diff --git a/execution/realtime_trader.py b/execution/realtime_trader.py index be3ca63..c55c8d6 100644 --- a/execution/realtime_trader.py +++ b/execution/realtime_trader.py @@ -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) - diff --git a/run/realtime.py b/run/realtime.py index 7a7c42e..562ae4b 100644 --- a/run/realtime.py +++ b/run/realtime.py @@ -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() diff --git a/run/server.py b/run/server.py index 850974f..67d0090 100644 --- a/run/server.py +++ b/run/server.py @@ -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) diff --git a/scripts/daily_optimize.py b/scripts/daily_optimize.py new file mode 100755 index 0000000..55f0f19 --- /dev/null +++ b/scripts/daily_optimize.py @@ -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()) diff --git a/scripts/restart_ea.sh b/scripts/restart_ea.sh new file mode 100755 index 0000000..4718e1e --- /dev/null +++ b/scripts/restart_ea.sh @@ -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" diff --git a/strategies/ma_cross.py b/strategies/ma_cross.py index 1971138..a326d89 100644 --- a/strategies/ma_cross.py +++ b/strategies/ma_cross.py @@ -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} 信号生成结束 (无信号) ---")