Files
mt5_python_ea_suite/scripts/daily_optimize.py
T

308 lines
11 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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():
# ★ 单实例锁:防止 cron 每20分钟时上一轮还没跑完
import fcntl
lock_path = PROJECT_DIR / ".optimizer.lock"
lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o644)
try:
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
logger.warning("⚠️ 上一轮优化仍在运行,跳过本轮")
os.close(lock_fd)
return 0
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())