mirror of
https://github.com/silencesdg/mt5_python_ea_suite.git
synced 2026-07-28 11:17:43 +00:00
e1691c3c41
- 新增适应度门槛: min_backtest_fitness=95, 适应度<95暂停开仓 - 新增 SwingPointRetest 策略替代 Turtle - 新增 monday_reset.py 周重置脚本 - exit_rules: 拖尾止损相对回撤模式 - market_state: 趋势检测优化 - position: 一票制并发锁+合约规格缓存 - optimize: Optuna 替代 DEAP 遗传算法 - realtime_trader: 适应度门槛+同向递增 - weights: 动态权重管理 - cron_optimize: PYTHONPATH 修复 - .gitignore: 排除生成文件
297 lines
10 KiB
Python
Executable File
297 lines
10 KiB
Python
Executable File
#!/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, fitness)"""
|
||
from execution.optimize import run_optimizer as _run
|
||
logger.info("🧬 开始遗传算法优化...")
|
||
best_params, fitness = _run()
|
||
logger.info(f"✅ 优化完成 适应度={fitness:.2f}")
|
||
return best_params, fitness
|
||
|
||
|
||
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, fitness: float = 0.0):
|
||
"""将优化结果写回 config.py"""
|
||
content = CONFIG_PATH.read_text(encoding="utf-8")
|
||
|
||
# ═══ RISK_CONFIG — 手动设定,不进优化器 ═══
|
||
# (optimizer.py PARAM_SPACE 已移除风控基因,此 map 清空)
|
||
risk_map = {
|
||
}
|
||
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"),
|
||
# SwingPointRetestStrategy
|
||
"swing_point_left_bars": ("swing_point", "left_bars"),
|
||
"swing_point_right_bars": ("swing_point", "right_bars"),
|
||
"swing_point_tolerance_pct": ("swing_point", "tolerance_pct"),
|
||
"swing_point_num_swings": ("swing_point", "num_swings"),
|
||
# 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_SwingPointRetestStrategy": "swing_point",
|
||
"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
|
||
)
|
||
|
||
# ═══ LAST_OPTIMIZATION_FITNESS ═══
|
||
content = re.sub(
|
||
r'LAST_OPTIMIZATION_FITNESS\s*=\s*[\d.\-e]+',
|
||
f'LAST_OPTIMIZATION_FITNESS = {fitness:.2f}',
|
||
content
|
||
)
|
||
|
||
CONFIG_PATH.write_text(content, encoding="utf-8")
|
||
logger.info("✏️ 配置已更新")
|
||
|
||
|
||
def restart_ea():
|
||
"""杀掉旧 EA → 清日志 → 启动新 EA(应用优化后的配置)"""
|
||
import subprocess
|
||
logger.info("🔄 重启 EA...")
|
||
|
||
# 杀旧进程,等锁释放再启动新的
|
||
subprocess.run(["pkill", "-f", "python.*run/realtime.py"], capture_output=True)
|
||
import time; time.sleep(2)
|
||
subprocess.run(["pkill", "-9", "-f", "python.*run/realtime.py"], capture_output=True)
|
||
# 确认旧进程已死 + 锁已释放(轮询最多等 5 秒)
|
||
lock_file = PROJECT_DIR / ".ea.lock"
|
||
import fcntl as _fcntl
|
||
for _ in range(10):
|
||
time.sleep(0.5)
|
||
try:
|
||
fd = os.open(str(lock_file), os.O_RDONLY)
|
||
_fcntl.flock(fd, _fcntl.LOCK_EX | _fcntl.LOCK_NB)
|
||
os.close(fd) # 立即释放,只是测试
|
||
break
|
||
except (BlockingIOError, OSError):
|
||
pass
|
||
else:
|
||
logger.warning("⚠️ 旧进程锁未释放,强制启动(旧进程可能僵死)")
|
||
|
||
# 清空旧交易记录
|
||
for f in PROJECT_DIR.glob("realtime_trades_*"):
|
||
f.unlink(missing_ok=True)
|
||
|
||
# 清日志
|
||
log_file = PROJECT_DIR / "logs" / "strategy.log"
|
||
log_file.write_text("")
|
||
|
||
# 启动新 EA(后台)
|
||
log = open(log_file, "a")
|
||
proc = subprocess.Popen(
|
||
[sys.executable, "run/realtime.py"],
|
||
cwd=str(PROJECT_DIR),
|
||
stdout=log, stderr=subprocess.STDOUT,
|
||
)
|
||
pid_file = PROJECT_DIR / ".ea_pid"
|
||
pid_file.write_text(str(proc.pid))
|
||
logger.info(f"✅ EA 已重启 PID={proc.pid}")
|
||
|
||
|
||
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, fitness = run_optimizer()
|
||
except Exception as e:
|
||
logger.error(f"优化失败: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return 1
|
||
|
||
# 3. 写入 config.py(含适应度)
|
||
update_config(best_params, fitness)
|
||
|
||
# 4. ★ EA 通过 config.reload() 自动热加载,无需重启
|
||
logger.info("✅ 每日优化流程完成 (EA 将在下个周期自动读取新配置)")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|