mirror of
https://github.com/silencesdg/mt5_python_ea_suite.git
synced 2026-07-28 11:17:43 +00:00
feat: 适应度门槛95+swing_point策略+多项改进
- 新增适应度门槛: 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: 排除生成文件
This commit is contained in:
+132
-282
@@ -1,21 +1,21 @@
|
||||
"""遗传算法优化器(重写版)
|
||||
"""贝叶斯优化器(Optuna TPE)
|
||||
高效替代 DEAP 遗传算法,50次试验 ≈ 2分钟收敛
|
||||
|
||||
关键改进:
|
||||
1. 权重基因通过 individual_weights → MarketStateAnalyzer.get_strategy_weights() 真正生效
|
||||
2. 使用 StrategyRegistry 单例消,除重复的策略列表
|
||||
3. 使用 MultiTimeframeDataStore 支持正确的多周期市场状态分析
|
||||
4. evaluate_fitness 逐 bar 调用 get_market_state(bar_index) 获取每根bar的动态权重
|
||||
1. TPE sampler 建模适应度曲面,采样效率高 3-5 倍
|
||||
2. 权重基因通过 individual_weights → MarketStateAnalyzer.get_strategy_weights() 真正生效
|
||||
3. evaluate_fitness 逐 bar 调用 get_market_state(bar_index) 获取每根bar的动态权重
|
||||
"""
|
||||
|
||||
import random
|
||||
import numpy as np
|
||||
from deap import base, creator, tools, algorithms
|
||||
import multiprocessing
|
||||
import pandas as pd
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
import optuna
|
||||
|
||||
SEED = 42
|
||||
random.seed(SEED)
|
||||
@@ -31,7 +31,7 @@ from core.signal.combiner import SignalCombiner
|
||||
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,
|
||||
RISK_CONFIG,
|
||||
MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS,
|
||||
DATA_PROVIDER_MODE, REMOTE_SERVER_HOST, REMOTE_SERVER_PORT
|
||||
)
|
||||
@@ -45,8 +45,6 @@ import strategies # noqa: F401
|
||||
|
||||
# ── 全局缓存 ──
|
||||
_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([
|
||||
@@ -61,15 +59,7 @@ _MT5_RATES_DTYPE = np.dtype([
|
||||
])
|
||||
|
||||
|
||||
def init_worker():
|
||||
"""多进程worker初始化:抑制日志噪音"""
|
||||
import logging
|
||||
logging.getLogger().setLevel(logging.WARNING)
|
||||
for name in ["StrategyLogger", "PositionManager", "RiskController", "DataProvider"]:
|
||||
logging.getLogger(name).setLevel(logging.WARNING)
|
||||
|
||||
|
||||
# ── 参数定义(与旧版一致,保持兼容)──
|
||||
# ── 参数定义 ──
|
||||
PARAMETER_DEFINITIONS = [
|
||||
# MACrossStrategy
|
||||
{'name': 'ma_cross_short_window', 'type': 'int', 'min': 3, 'max': 15, 'strategy': 'MACrossStrategy'},
|
||||
@@ -93,8 +83,11 @@ PARAMETER_DEFINITIONS = [
|
||||
{'name': 'momentum_breakout_momentum_period', 'type': 'int', 'min': 5, 'max': 30, 'strategy': 'MomentumBreakoutStrategy'},
|
||||
# KDJStrategy
|
||||
{'name': 'kdj_period', 'type': 'int', 'min': 5, 'max': 21, 'strategy': 'KDJStrategy'},
|
||||
# TurtleStrategy
|
||||
{'name': 'turtle_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'TurtleStrategy'},
|
||||
# SwingPointRetestStrategy — 前高前低回踩
|
||||
{'name': 'swing_point_left_bars', 'type': 'int', 'min': 2, 'max': 5, 'strategy': 'SwingPointRetestStrategy'},
|
||||
{'name': 'swing_point_right_bars', 'type': 'int', 'min': 2, 'max': 5, 'strategy': 'SwingPointRetestStrategy'},
|
||||
{'name': 'swing_point_tolerance_pct', 'type': 'float', 'min': 0.0003, 'max': 0.002, 'strategy': 'SwingPointRetestStrategy'},
|
||||
{'name': 'swing_point_num_swings', 'type': 'int', 'min': 1, 'max': 3, 'strategy': 'SwingPointRetestStrategy'},
|
||||
# WaveTheoryStrategy
|
||||
{'name': 'wave_ema_short', 'type': 'int', 'min': 3, 'max': 10, 'strategy': 'WaveTheoryStrategy'},
|
||||
{'name': 'wave_ema_medium', 'type': 'int', 'min': 8, 'max': 20, 'strategy': 'WaveTheoryStrategy'},
|
||||
@@ -110,14 +103,7 @@ PARAMETER_DEFINITIONS = [
|
||||
# Signal Thresholds
|
||||
{'name': 'buy_threshold', 'type': 'float', 'min': 0.5, 'max': 3.0, 'strategy': 'signal'},
|
||||
{'name': 'sell_threshold', 'type': 'float', 'min': -3.0, 'max': -0.5, 'strategy': 'signal'},
|
||||
# Risk Management(范围适配保证金%基准)
|
||||
{'name': 'stop_loss_pct', 'type': 'float', 'min': -1.00, 'max': -0.10, 'strategy': 'risk'},
|
||||
{'name': 'profit_retracement_pct', 'type': 'float', 'min': 0.10, 'max': 0.80, 'strategy': 'risk'},
|
||||
{'name': 'min_profit_for_trailing', 'type': 'float', 'min': 0.30, 'max': 2.00, 'strategy': 'risk'},
|
||||
{'name': 'take_profit_pct', 'type': 'float', 'min': 0.50, 'max': 3.00, 'strategy': 'risk'},
|
||||
{'name': 'max_holding_minutes', 'type': 'int', 'min': 30, 'max': 180, 'strategy': 'risk'},
|
||||
{'name': 'min_profit_for_time_exit', 'type': 'float', 'min': 0.02, 'max': 0.20, 'strategy': 'risk'},
|
||||
# ★ Strategy Weights — 这些基因现在真正影响适应度
|
||||
# ★ Strategy Weights
|
||||
{'name': 'weight_MACrossStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_RSIStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_BollingerStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
@@ -125,7 +111,7 @@ PARAMETER_DEFINITIONS = [
|
||||
{'name': 'weight_MomentumBreakoutStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_MACDStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_KDJStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_TurtleStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_SwingPointRetestStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_DailyBreakoutStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
{'name': 'weight_WaveTheoryStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
||||
# Market State Analysis parameters
|
||||
@@ -149,7 +135,6 @@ PARAMETER_DEFINITIONS = [
|
||||
{'name': 'confidence_medium', 'type': 'float', 'min': 0.3, 'max': 0.7, 'strategy': 'confidence'},
|
||||
]
|
||||
|
||||
# class_name → config_key 映射(从 StrategyRegistry 获取)
|
||||
_class_name_to_config_key = {
|
||||
"MACrossStrategy": "ma_cross",
|
||||
"RSIStrategy": "rsi",
|
||||
@@ -158,27 +143,27 @@ _class_name_to_config_key = {
|
||||
"MomentumBreakoutStrategy": "momentum_breakout",
|
||||
"MACDStrategy": "macd",
|
||||
"KDJStrategy": "kdj",
|
||||
"TurtleStrategy": "turtle",
|
||||
"SwingPointRetestStrategy": "swing_point",
|
||||
"DailyBreakoutStrategy": "daily_breakout",
|
||||
"WaveTheoryStrategy": "wave_theory",
|
||||
}
|
||||
|
||||
|
||||
def _parse_individual(individual):
|
||||
"""解析个体基因为命名参数字典,并裁剪到定义范围"""
|
||||
def _trial_to_params(trial: optuna.Trial) -> dict:
|
||||
"""从 Optuna trial 提取参数 → 兼容旧 _parse_individual 格式"""
|
||||
parsed = {}
|
||||
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
|
||||
val = individual[idx]
|
||||
for param_def in PARAMETER_DEFINITIONS:
|
||||
name = param_def['name']
|
||||
if param_def['type'] == 'int':
|
||||
val = int(round(val))
|
||||
# 裁剪到 [min, max] 防止变异越界
|
||||
val = max(param_def['min'], min(param_def['max'], val))
|
||||
parsed[param_def['name']] = val
|
||||
val = trial.suggest_int(name, param_def['min'], param_def['max'])
|
||||
else:
|
||||
val = trial.suggest_float(name, param_def['min'], param_def['max'])
|
||||
parsed[name] = val
|
||||
return parsed
|
||||
|
||||
|
||||
def _extract_strategy_params(parsed: dict) -> dict:
|
||||
"""从解析后的基因提取策略参数字典"""
|
||||
"""从解析后的参数提取策略参数字典"""
|
||||
strategy_params = {}
|
||||
for param_def in PARAMETER_DEFINITIONS:
|
||||
strategy_name = param_def.get('strategy')
|
||||
@@ -188,57 +173,37 @@ def _extract_strategy_params(parsed: dict) -> dict:
|
||||
if strategy_name not in strategy_params:
|
||||
strategy_params[strategy_name] = {}
|
||||
|
||||
# 参数名映射(保持与旧版兼容)
|
||||
param_name = param_def['name']
|
||||
if param_name == 'ma_cross_short_window':
|
||||
param_name = 'short_window'
|
||||
elif param_name == 'ma_cross_long_window':
|
||||
param_name = 'long_window'
|
||||
elif param_name == 'rsi_period':
|
||||
param_name = 'period'
|
||||
elif param_name == 'rsi_overbought':
|
||||
param_name = 'overbought'
|
||||
elif param_name == 'rsi_oversold':
|
||||
param_name = 'oversold'
|
||||
elif param_name == 'bollinger_period':
|
||||
param_name = 'period'
|
||||
elif param_name == 'bollinger_std_dev':
|
||||
param_name = 'std_dev'
|
||||
elif param_name == 'macd_fast_ema':
|
||||
param_name = 'fast_ema'
|
||||
elif param_name == 'macd_slow_ema':
|
||||
param_name = 'slow_ema'
|
||||
elif param_name == 'macd_signal_period':
|
||||
param_name = 'signal_period'
|
||||
elif param_name == 'mean_reversion_period':
|
||||
param_name = 'period'
|
||||
elif param_name == 'mean_reversion_std_dev':
|
||||
param_name = 'std_dev'
|
||||
elif param_name == 'momentum_breakout_period':
|
||||
param_name = 'period'
|
||||
elif param_name == 'momentum_breakout_momentum_period':
|
||||
param_name = 'momentum_period'
|
||||
elif param_name == 'kdj_period':
|
||||
param_name = 'period'
|
||||
elif param_name == 'turtle_period':
|
||||
param_name = 'period'
|
||||
if param_name == 'ma_cross_short_window': param_name = 'short_window'
|
||||
elif param_name == 'ma_cross_long_window': param_name = 'long_window'
|
||||
elif param_name == 'rsi_period': param_name = 'period'
|
||||
elif param_name == 'rsi_overbought': param_name = 'overbought'
|
||||
elif param_name == 'rsi_oversold': param_name = 'oversold'
|
||||
elif param_name == 'bollinger_period': param_name = 'period'
|
||||
elif param_name == 'bollinger_std_dev': param_name = 'std_dev'
|
||||
elif param_name == 'macd_fast_ema': param_name = 'fast_ema'
|
||||
elif param_name == 'macd_slow_ema': param_name = 'slow_ema'
|
||||
elif param_name == 'macd_signal_period': param_name = 'signal_period'
|
||||
elif param_name == 'mean_reversion_period': param_name = 'period'
|
||||
elif param_name == 'mean_reversion_std_dev': param_name = 'std_dev'
|
||||
elif param_name == 'momentum_breakout_period': param_name = 'period'
|
||||
elif param_name == 'momentum_breakout_momentum_period': param_name = 'momentum_period'
|
||||
elif param_name == 'kdj_period': param_name = 'period'
|
||||
elif param_name == 'swing_point_left_bars': param_name = 'left_bars'
|
||||
elif param_name == 'swing_point_right_bars': param_name = 'right_bars'
|
||||
elif param_name == 'swing_point_tolerance_pct': param_name = 'tolerance_pct'
|
||||
elif param_name == 'swing_point_num_swings': param_name = 'num_swings'
|
||||
elif param_name.startswith("wave_"):
|
||||
param_name = param_name[5:]
|
||||
if param_name == "period":
|
||||
param_name = "wave_period"
|
||||
elif param_name == 'daily_breakout_bars_count':
|
||||
param_name = 'bars_count'
|
||||
if param_name == "period": param_name = "wave_period"
|
||||
elif param_name == 'daily_breakout_bars_count': param_name = 'bars_count'
|
||||
|
||||
strategy_params[strategy_name][param_name] = parsed[param_def['name']]
|
||||
return strategy_params
|
||||
|
||||
|
||||
def _extract_weight_genes(parsed: dict) -> dict:
|
||||
"""★ 提取权重基因 → {config_key: weight}
|
||||
|
||||
这些权重会通过 individual_weights 参数传递给
|
||||
MarketStateAnalyzer.get_strategy_weights(),使权重基因真正生效。
|
||||
"""
|
||||
"""提取权重基因 → {config_key: weight}"""
|
||||
weight_genes = {}
|
||||
for param_def in PARAMETER_DEFINITIONS:
|
||||
if param_def.get('strategy') != 'weight':
|
||||
@@ -258,7 +223,6 @@ def _precompute_h1_states(multi_tf: MultiTimeframeDataStore, analyzer: MarketSta
|
||||
m1_index = multi_tf.main_df.index
|
||||
h1_index = h1_df.index
|
||||
|
||||
# 计算每个H1 bar的状态
|
||||
h1_states = []
|
||||
for i in range(len(h1_df)):
|
||||
lookback = analyzer.trend_period + 10
|
||||
@@ -267,11 +231,9 @@ def _precompute_h1_states(multi_tf: MultiTimeframeDataStore, analyzer: MarketSta
|
||||
state, conf = analyzer._calculate_state_from_df(df_slice)
|
||||
h1_states.append((state, conf))
|
||||
|
||||
# 映射到M1时间轴
|
||||
m1_states = []
|
||||
h1_pos = 0
|
||||
first_h1_time = h1_index[0]
|
||||
|
||||
for m1_time in m1_index:
|
||||
if m1_time < first_h1_time:
|
||||
m1_states.append(("none", 0.0))
|
||||
@@ -282,44 +244,34 @@ def _precompute_h1_states(multi_tf: MultiTimeframeDataStore, analyzer: MarketSta
|
||||
m1_states.append(h1_states[h1_pos])
|
||||
else:
|
||||
m1_states.append(("none", 0.0))
|
||||
|
||||
return m1_states
|
||||
|
||||
|
||||
def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
|
||||
_signals_df_unused=None) -> tuple:
|
||||
"""★ 重写的适应度函数 — 所有权重基因、策略参数和风控参数真正生效
|
||||
def evaluate_fitness(trial: optuna.Trial, multi_tf: MultiTimeframeDataStore) -> float:
|
||||
"""★ Optuna 适应度函数 — 所有权重/策略/趋势参数真正生效"""
|
||||
parsed = _trial_to_params(trial)
|
||||
|
||||
三个关键基因全部生效:
|
||||
1. 策略参数 → 重新实例化策略并 run_backtest → 影响信号序列
|
||||
2. 权重基因 → per-bar get_strategy_weights(individual_weights=...) → 影响信号组合
|
||||
3. 风控参数 → PositionManager 在回测中真正使用
|
||||
"""
|
||||
parsed = _parse_individual(individual)
|
||||
|
||||
# 1. 提取策略参数
|
||||
# 1. 策略参数
|
||||
strategy_params = _extract_strategy_params(parsed)
|
||||
|
||||
# 2. ★ 提取权重基因(核心修复)
|
||||
# 2. 权重基因
|
||||
weight_genes = _extract_weight_genes(parsed)
|
||||
|
||||
# 3. 提取风控参数
|
||||
# 3. 风控参数(从 config 读,不进优化器)
|
||||
risk_params = {
|
||||
'stop_loss_pct': parsed.get('stop_loss_pct', RISK_CONFIG.get('stop_loss_pct', -0.01)),
|
||||
'profit_retracement_pct': parsed.get('profit_retracement_pct', RISK_CONFIG.get('profit_retracement_pct', 0.10)),
|
||||
'min_profit_for_trailing': parsed.get('min_profit_for_trailing', RISK_CONFIG.get('min_profit_for_trailing', 0.01)),
|
||||
'take_profit_pct': parsed.get('take_profit_pct', RISK_CONFIG.get('take_profit_pct', 0.20)),
|
||||
'max_holding_minutes': parsed.get('max_holding_minutes', RISK_CONFIG.get('max_holding_minutes', 60)),
|
||||
'min_profit_for_time_exit': parsed.get('min_profit_for_time_exit', RISK_CONFIG.get('min_profit_for_time_exit', 0.005)),
|
||||
'max_daily_loss': parsed.get('max_daily_loss', RISK_CONFIG.get('max_daily_loss', -0.30)),
|
||||
'stop_loss_pct': RISK_CONFIG.get('stop_loss_pct', -0.50),
|
||||
'profit_retracement_pct': RISK_CONFIG.get('profit_retracement_pct', 0.35),
|
||||
'min_profit_for_trailing': RISK_CONFIG.get('min_profit_for_trailing', 0.70),
|
||||
'take_profit_pct': RISK_CONFIG.get('take_profit_pct', 1.00),
|
||||
'max_holding_minutes': RISK_CONFIG.get('max_holding_minutes', 0),
|
||||
'min_profit_for_time_exit': RISK_CONFIG.get('min_profit_for_time_exit', 0.005),
|
||||
'max_daily_loss': RISK_CONFIG.get('max_daily_loss', -0.30),
|
||||
}
|
||||
|
||||
# 4. 提取市场状态和趋势参数
|
||||
# 4. 市场状态和趋势参数
|
||||
market_params = {
|
||||
'trend_period': parsed.get('market_trend_period', MARKET_STATE_CONFIG.get('trend_period', 50)),
|
||||
'retracement_tolerance': parsed.get('market_retracement_tolerance', MARKET_STATE_CONFIG.get('retracement_tolerance', 0.30)),
|
||||
'volume_period': parsed.get('market_volume_period', MARKET_STATE_CONFIG.get('volume_period', 20)),
|
||||
'volume_ma_period': parsed.get('market_volume_ma_period', MARKET_STATE_CONFIG.get('volume_ma_period', 10)),
|
||||
'trend_period': parsed.get('market_trend_period', 50),
|
||||
'retracement_tolerance': parsed.get('market_retracement_tolerance', 0.30),
|
||||
'volume_period': parsed.get('market_volume_period', 20),
|
||||
'volume_ma_period': parsed.get('market_volume_ma_period', 10),
|
||||
'hourly_data_count': 100,
|
||||
}
|
||||
trend_weights = {
|
||||
@@ -340,7 +292,7 @@ def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
|
||||
'medium_confidence': parsed.get('confidence_medium', 0.4),
|
||||
}
|
||||
|
||||
# 5. 构建 MarketStateAnalyzer 并预计算H1状态
|
||||
# 5. MarketStateAnalyzer
|
||||
analyzer = MarketStateAnalyzer(
|
||||
timeframe=PERIOD_H1,
|
||||
market_state_params=market_params,
|
||||
@@ -350,8 +302,7 @@ def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
|
||||
)
|
||||
analyzer._precomputed_states = _precompute_h1_states(multi_tf, analyzer)
|
||||
|
||||
# 5.5. ★ 使用个体的策略参数重新实例化策略并重新计算信号
|
||||
# 将 class_name → {params} 映射为 config_key → {params}
|
||||
# 5.5 用个体策略参数重新计算信号
|
||||
strategy_params_by_key = {
|
||||
_class_name_to_config_key.get(cn, cn.lower()): params
|
||||
for cn, params in strategy_params.items()
|
||||
@@ -375,29 +326,19 @@ def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
|
||||
sell_th = parsed.get('sell_threshold', -1.5)
|
||||
total_bars = multi_tf.length
|
||||
|
||||
# 7. ★ 逐 bar 回测(权重基因真正影响信号组合)
|
||||
# 7. 逐 bar 回测
|
||||
for bar_index in range(total_bars):
|
||||
current_price = data_provider.get_current_price(SYMBOL)
|
||||
if not current_price:
|
||||
data_provider.tick()
|
||||
continue
|
||||
|
||||
# 获取当前 bar 的市场状态
|
||||
state, conf = analyzer.get_market_state(bar_index)
|
||||
weights = analyzer.get_strategy_weights(state, conf, individual_weights=weight_genes)
|
||||
|
||||
# ★ 权重基因在这里生效
|
||||
weights = analyzer.get_strategy_weights(
|
||||
state, conf, individual_weights=weight_genes
|
||||
)
|
||||
|
||||
# 组合当前 bar 的信号(使用个体参数重新计算的信号)
|
||||
bar_signals = {
|
||||
col: signals_df[col].iloc[bar_index]
|
||||
for col in signals_df.columns
|
||||
}
|
||||
bar_signals = {col: signals_df[col].iloc[bar_index] for col in signals_df.columns}
|
||||
final_signal = SignalCombiner.combine_at_bar(bar_signals, weights, buy_th, sell_th)
|
||||
|
||||
# 执行交易
|
||||
if final_signal == 1:
|
||||
pm.open_position("buy", current_price, 1.0, dry_run=True)
|
||||
elif final_signal == -1:
|
||||
@@ -407,20 +348,16 @@ def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
|
||||
data_provider.tick()
|
||||
|
||||
# 8. 适应度 = 总盈亏
|
||||
total_pnl = pm.total_equity - INITIAL_CAPITAL
|
||||
return (total_pnl,)
|
||||
return pm.total_equity - INITIAL_CAPITAL
|
||||
|
||||
|
||||
def _json_to_mt5_rates(rates_data):
|
||||
"""将远程API JSON rates 转换为 MT5 兼容的 numpy 结构化数组"""
|
||||
"""远程API JSON → 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),
|
||||
))
|
||||
records = [(r['time'], r['open'], r['high'], r['low'], r['close'],
|
||||
r.get('tick_volume', 0), r.get('spread', 0), r.get('real_volume', 0))
|
||||
for r in rates_data]
|
||||
return np.array(records, dtype=_MT5_RATES_DTYPE)
|
||||
|
||||
|
||||
@@ -430,152 +367,96 @@ def load_historical_data():
|
||||
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)
|
||||
)
|
||||
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("获取历史数据失败")
|
||||
|
||||
if len(rates) > OPTIMIZER_COUNT:
|
||||
rates = rates[-OPTIMIZER_COUNT:]
|
||||
|
||||
store = MultiTimeframeDataStore()
|
||||
store.load_m1_data(rates)
|
||||
|
||||
# 预生成H1数据
|
||||
store.ensure_timeframe(PERIOD_H1)
|
||||
|
||||
return store
|
||||
|
||||
|
||||
def precompute_all_signals(multi_tf: MultiTimeframeDataStore,
|
||||
strategy_params: dict = None) -> pd.DataFrame:
|
||||
"""使用默认参数预计算所有策略的回测信号"""
|
||||
registry = StrategyRegistry()
|
||||
strategies = registry.instantiate_all(SYMBOL, TIMEFRAME, strategy_params)
|
||||
signals_df = pd.DataFrame(index=multi_tf.main_df.index)
|
||||
def save_optimization_results(best_params: dict, best_fitness: float, study: optuna.Study):
|
||||
"""保存优化结果(兼容旧格式)"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
import json
|
||||
|
||||
for config_key, strategy in strategies.items():
|
||||
try:
|
||||
sig = strategy.run_backtest(multi_tf.main_df)
|
||||
signals_df[config_key] = sig if sig is not None else pd.Series(0, index=signals_df.index)
|
||||
except Exception:
|
||||
signals_df[config_key] = pd.Series(0, index=signals_df.index)
|
||||
results = {
|
||||
'best_fitness': best_fitness,
|
||||
'best_params': best_params,
|
||||
'n_trials': len(study.trials),
|
||||
'optimizer': 'optuna_tpe',
|
||||
}
|
||||
json_path = f"optimization_results_{timestamp}.json"
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
|
||||
logger.info(f"结果已保存: {json_path}")
|
||||
|
||||
return signals_df
|
||||
txt_path = f"optimization_report_{timestamp}.txt"
|
||||
with open(txt_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"优化完成时间: {datetime.now()}\n")
|
||||
f.write(f"最佳适应度: {best_fitness:.2f}\n")
|
||||
f.write(f"试验次数: {len(study.trials)}\n\n")
|
||||
f.write("最佳参数:\n")
|
||||
for key, value in best_params.items():
|
||||
f.write(f" {key}: {value}\n")
|
||||
logger.info(f"报告已保存: {txt_path}")
|
||||
|
||||
|
||||
def run_optimizer():
|
||||
"""运行遗传算法优化(所有基因真正生效)"""
|
||||
"""★ Optuna TPE 贝叶斯优化(替代 DEAP 遗传算法)"""
|
||||
try:
|
||||
# 1. 加载数据(一次性)
|
||||
# 1. 加载数据
|
||||
logger.info("加载历史数据...")
|
||||
multi_tf = load_historical_data()
|
||||
logger.info(f"数据加载完成: {multi_tf.length} 条M1数据")
|
||||
|
||||
# 2. DEAP 设置(策略信号在 evaluate_fitness 内部逐个体重新计算)
|
||||
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
|
||||
creator.create("Individual", list, fitness=creator.FitnessMax)
|
||||
# 2. Optuna study
|
||||
sampler = optuna.samplers.TPESampler(seed=SEED, n_startup_trials=10)
|
||||
study = optuna.create_study(direction="maximize", sampler=sampler)
|
||||
|
||||
toolbox = base.Toolbox()
|
||||
|
||||
for i, param_def in enumerate(PARAMETER_DEFINITIONS):
|
||||
if param_def['type'] == 'int':
|
||||
toolbox.register(f"attr_param_{i}", random.randint, param_def['min'], param_def['max'])
|
||||
else:
|
||||
toolbox.register(f"attr_param_{i}", random.uniform, param_def['min'], param_def['max'])
|
||||
|
||||
toolbox.register("individual", tools.initIterate, creator.Individual,
|
||||
lambda: [toolbox.__getattribute__(f"attr_param_{j}")()
|
||||
for j in range(len(PARAMETER_DEFINITIONS))])
|
||||
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
|
||||
|
||||
toolbox.register("evaluate", evaluate_fitness,
|
||||
multi_tf=multi_tf)
|
||||
toolbox.register("mate", tools.cxTwoPoint)
|
||||
toolbox.register("mutate", tools.mutGaussian,
|
||||
mu=GENETIC_OPTIMIZER_CONFIG["mutation_mu"],
|
||||
sigma=GENETIC_OPTIMIZER_CONFIG["mutation_sigma"],
|
||||
indpb=GENETIC_OPTIMIZER_CONFIG["mutation_indpb"])
|
||||
toolbox.register("select", tools.selTournament,
|
||||
tournsize=GENETIC_OPTIMIZER_CONFIG["tournament_size"])
|
||||
|
||||
# 多进程
|
||||
if GENETIC_OPTIMIZER_CONFIG["enable_multiprocessing"]:
|
||||
pool = multiprocessing.Pool(
|
||||
processes=GENETIC_OPTIMIZER_CONFIG["processes"],
|
||||
initializer=init_worker
|
||||
# 3. 优化(4进程并行,静默回测日志避免 I/O 争用)
|
||||
optuna.logging.set_verbosity(optuna.logging.WARNING)
|
||||
import logging as _logging
|
||||
_old_level = logger.level
|
||||
_old_handler_levels = [h.level for h in logger.handlers]
|
||||
logger.setLevel(_logging.WARNING)
|
||||
for h in logger.handlers:
|
||||
h.setLevel(_logging.WARNING)
|
||||
logger.info(f"开始贝叶斯优化: 50 次试验, TPE sampler (回测日志已静默)")
|
||||
try:
|
||||
study.optimize(
|
||||
lambda trial: evaluate_fitness(trial, multi_tf),
|
||||
n_trials=50,
|
||||
n_jobs=4,
|
||||
show_progress_bar=False,
|
||||
)
|
||||
toolbox.register("map", pool.map)
|
||||
else:
|
||||
toolbox.register("map", map)
|
||||
finally:
|
||||
logger.setLevel(_old_level)
|
||||
for h, lvl in zip(logger.handlers, _old_handler_levels):
|
||||
h.setLevel(lvl)
|
||||
|
||||
# 4. 统计
|
||||
stats = tools.Statistics(lambda ind: ind.fitness.values)
|
||||
stats.register("avg", np.mean)
|
||||
stats.register("std", np.std)
|
||||
stats.register("min", np.min)
|
||||
stats.register("max", np.max)
|
||||
# 4. 结果
|
||||
best_fitness = study.best_value
|
||||
best_params = study.best_params
|
||||
logger.info(f"优化完成: 最佳适应度={best_fitness:.2f}, 试验次数={len(study.trials)}")
|
||||
|
||||
# 5. 运行
|
||||
population = toolbox.population(n=GENETIC_OPTIMIZER_CONFIG["population_size"])
|
||||
ngen = GENETIC_OPTIMIZER_CONFIG["generations"]
|
||||
cxpb = GENETIC_OPTIMIZER_CONFIG["crossover_probability"]
|
||||
mutpb = GENETIC_OPTIMIZER_CONFIG["mutation_probability"]
|
||||
save_optimization_results(best_params, best_fitness, study)
|
||||
|
||||
logger.info(f"开始进化: 种群 {len(population)}, {ngen} 代")
|
||||
generation_info = []
|
||||
|
||||
for gen in range(ngen):
|
||||
offspring = toolbox.select(population, len(population))
|
||||
offspring = algorithms.varAnd(offspring, toolbox, cxpb, mutpb)
|
||||
fits = toolbox.map(toolbox.evaluate, offspring)
|
||||
|
||||
for fit, ind in zip(fits, offspring):
|
||||
ind.fitness.values = fit
|
||||
|
||||
population[:] = offspring
|
||||
|
||||
best = tools.selBest(population, k=1)[0]
|
||||
best_fit = best.fitness.values[0]
|
||||
avg_fit = np.mean([ind.fitness.values[0] for ind in population])
|
||||
|
||||
if GENETIC_OPTIMIZER_CONFIG.get("save_generation_info", True):
|
||||
generation_info.append({
|
||||
'generation': gen + 1,
|
||||
'best_fitness': best_fit,
|
||||
'avg_fitness': avg_fit,
|
||||
})
|
||||
|
||||
if GENETIC_OPTIMIZER_CONFIG.get("verbose", True):
|
||||
logger.info(f"代数 {gen + 1}/{ngen}: 最佳={best_fit:.2f}, 平均={avg_fit:.2f}")
|
||||
|
||||
# 6. 结果
|
||||
if GENETIC_OPTIMIZER_CONFIG["enable_multiprocessing"]:
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
best_individual = tools.selBest(population, k=1)[0]
|
||||
best_fitness = best_individual.fitness.values[0]
|
||||
|
||||
logger.info(f"优化完成: 最佳适应度={best_fitness:.2f}")
|
||||
save_optimization_results(best_individual, best_fitness, generation_info)
|
||||
|
||||
return _parse_individual(best_individual), best_fitness
|
||||
return best_params, best_fitness
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
@@ -583,36 +464,5 @@ def run_optimizer():
|
||||
raise
|
||||
|
||||
|
||||
def save_optimization_results(best_individual, best_fitness, generation_info):
|
||||
"""保存优化结果"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
parsed = _parse_individual(best_individual)
|
||||
|
||||
# JSON
|
||||
import json
|
||||
results = {
|
||||
'best_fitness': best_fitness,
|
||||
'best_params': parsed,
|
||||
'generation_info': generation_info,
|
||||
}
|
||||
json_path = f"optimization_results_{timestamp}.json"
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
|
||||
logger.info(f"结果已保存: {json_path}")
|
||||
|
||||
# TXT报告
|
||||
txt_path = f"optimization_report_{timestamp}.txt"
|
||||
with open(txt_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"优化完成时间: {datetime.now()}\n")
|
||||
f.write(f"最佳适应度: {best_fitness:.2f}\n\n")
|
||||
f.write("最佳参数:\n")
|
||||
for key, value in parsed.items():
|
||||
f.write(f" {key}: {value}\n")
|
||||
f.write("\n代数统计:\n")
|
||||
for gi in generation_info:
|
||||
f.write(f" 代数 {gi['generation']}: 最佳={gi['best_fitness']:.2f}, 平均={gi['avg_fitness']:.2f}\n")
|
||||
logger.info(f"报告已保存: {txt_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_optimizer()
|
||||
|
||||
Reference in New Issue
Block a user