2026-05-11 12:00:45 +08:00
|
|
|
"""遗传算法优化器(重写版)
|
|
|
|
|
|
|
|
|
|
关键改进:
|
|
|
|
|
1. 权重基因通过 individual_weights → MarketStateAnalyzer.get_strategy_weights() 真正生效
|
|
|
|
|
2. 使用 StrategyRegistry 单例消,除重复的策略列表
|
|
|
|
|
3. 使用 MultiTimeframeDataStore 支持正确的多周期市场状态分析
|
|
|
|
|
4. 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
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from tqdm import tqdm
|
|
|
|
|
|
|
|
|
|
SEED = 42
|
|
|
|
|
random.seed(SEED)
|
|
|
|
|
np.random.seed(SEED)
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
|
|
|
|
from core.data import MultiTimeframeDataStore, BacktestDataProvider
|
|
|
|
|
from core.risk.market_state import MarketStateAnalyzer
|
|
|
|
|
from core.risk.position import PositionManager
|
|
|
|
|
from core.signal.registry import StrategyRegistry
|
|
|
|
|
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,
|
2026-05-14 13:33:23 +08:00
|
|
|
MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS,
|
|
|
|
|
DATA_PROVIDER_MODE, REMOTE_SERVER_HOST, REMOTE_SERVER_PORT
|
2026-05-11 12:00:45 +08:00
|
|
|
)
|
|
|
|
|
from utils.constants import PERIOD_H1
|
|
|
|
|
from core.utils import get_rates, initialize, shutdown
|
2026-05-14 13:33:23 +08:00
|
|
|
from core.data.remote import RemoteDataProvider
|
2026-05-11 12:00:45 +08:00
|
|
|
from logger import logger
|
|
|
|
|
|
|
|
|
|
# 导入策略模块确保 StrategyRegistry 已注册
|
|
|
|
|
import strategies # noqa: F401
|
|
|
|
|
|
|
|
|
|
# ── 全局缓存 ──
|
|
|
|
|
_multi_tf: MultiTimeframeDataStore | None = None
|
|
|
|
|
_cached_signals: pd.DataFrame | None = None
|
|
|
|
|
_registry: StrategyRegistry | None = None
|
|
|
|
|
|
2026-05-14 13:33:23 +08:00
|
|
|
# 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'),
|
|
|
|
|
])
|
|
|
|
|
|
2026-05-11 12:00:45 +08:00
|
|
|
|
|
|
|
|
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'},
|
|
|
|
|
{'name': 'ma_cross_long_window', 'type': 'int', 'min': 10, 'max': 60, 'strategy': 'MACrossStrategy'},
|
|
|
|
|
# RSIStrategy
|
|
|
|
|
{'name': 'rsi_period', 'type': 'int', 'min': 7, 'max': 21, 'strategy': 'RSIStrategy'},
|
|
|
|
|
{'name': 'rsi_overbought', 'type': 'int', 'min': 65, 'max': 80, 'strategy': 'RSIStrategy'},
|
|
|
|
|
{'name': 'rsi_oversold', 'type': 'int', 'min': 20, 'max': 35, 'strategy': 'RSIStrategy'},
|
|
|
|
|
# BollingerStrategy
|
|
|
|
|
{'name': 'bollinger_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'BollingerStrategy'},
|
|
|
|
|
{'name': 'bollinger_std_dev', 'type': 'float', 'min': 1.5, 'max': 3.0, 'strategy': 'BollingerStrategy'},
|
|
|
|
|
# MACDStrategy
|
|
|
|
|
{'name': 'macd_fast_ema', 'type': 'int', 'min': 8, 'max': 20, 'strategy': 'MACDStrategy'},
|
|
|
|
|
{'name': 'macd_slow_ema', 'type': 'int', 'min': 20, 'max': 35, 'strategy': 'MACDStrategy'},
|
|
|
|
|
{'name': 'macd_signal_period', 'type': 'int', 'min': 5, 'max': 15, 'strategy': 'MACDStrategy'},
|
|
|
|
|
# MeanReversionStrategy
|
|
|
|
|
{'name': 'mean_reversion_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MeanReversionStrategy'},
|
|
|
|
|
{'name': 'mean_reversion_std_dev', 'type': 'float', 'min': 1.5, 'max': 3.0, 'strategy': 'MeanReversionStrategy'},
|
|
|
|
|
# MomentumBreakoutStrategy
|
|
|
|
|
{'name': 'momentum_breakout_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MomentumBreakoutStrategy'},
|
|
|
|
|
{'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'},
|
|
|
|
|
# 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'},
|
|
|
|
|
{'name': 'wave_ema_long', 'type': 'int', 'min': 20, 'max': 50, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
{'name': 'wave_period', 'type': 'int', 'min': 10, 'max': 40, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
{'name': 'wave_range_period', 'type': 'int', 'min': 10, 'max': 40, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
{'name': 'wave_adx_period', 'type': 'int', 'min': 10, 'max': 25, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
{'name': 'wave_momentum_period', 'type': 'int', 'min': 7, 'max': 21, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
{'name': 'wave_range_threshold', 'type': 'float', 'min': 0.002, 'max': 0.01, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
{'name': 'wave_adx_threshold', 'type': 'int', 'min': 15, 'max': 35, 'strategy': 'WaveTheoryStrategy'},
|
|
|
|
|
# DailyBreakoutStrategy
|
|
|
|
|
{'name': 'daily_breakout_bars_count', 'type': 'int', 'min': 720, 'max': 2880, 'strategy': 'DailyBreakoutStrategy'},
|
|
|
|
|
# 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': -0.05, 'max': -0.01, 'strategy': 'risk'},
|
|
|
|
|
{'name': 'profit_retracement_pct', 'type': 'float', 'min': 0.05, 'max': 0.20, 'strategy': 'risk'},
|
|
|
|
|
{'name': 'min_profit_for_trailing', 'type': 'float', 'min': 0.005, 'max': 0.02, 'strategy': 'risk'},
|
|
|
|
|
{'name': 'take_profit_pct', 'type': 'float', 'min': 0.10, 'max': 0.50, 'strategy': 'risk'},
|
|
|
|
|
{'name': 'max_holding_minutes', 'type': 'int', 'min': 30, 'max': 180, 'strategy': 'risk'},
|
|
|
|
|
{'name': 'min_profit_for_time_exit', 'type': 'float', 'min': 0.002, 'max': 0.01, 'strategy': 'risk'},
|
|
|
|
|
# ★ 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'},
|
|
|
|
|
{'name': 'weight_MeanReversionStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
|
|
|
|
|
{'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_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
|
|
|
|
|
{'name': 'market_trend_period', 'type': 'int', 'min': 20, 'max': 100, 'strategy': 'market_state'},
|
|
|
|
|
{'name': 'market_retracement_tolerance', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'market_state'},
|
|
|
|
|
{'name': 'market_volume_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'market_state'},
|
|
|
|
|
{'name': 'market_volume_ma_period', 'type': 'int', 'min': 5, 'max': 30, 'strategy': 'market_state'},
|
|
|
|
|
# Trend Indicator Weights
|
|
|
|
|
{'name': 'trend_price_breakout_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
|
|
|
|
|
{'name': 'trend_volume_confirmation_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
|
|
|
|
|
{'name': 'trend_momentum_oscillator_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
|
|
|
|
|
{'name': 'trend_moving_average_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
|
|
|
|
|
# Trend Thresholds
|
|
|
|
|
{'name': 'trend_strong_threshold', 'type': 'float', 'min': 0.4, 'max': 0.8, 'strategy': 'trend_thresholds'},
|
|
|
|
|
{'name': 'trend_weak_threshold', 'type': 'float', 'min': 0.2, 'max': 0.5, 'strategy': 'trend_thresholds'},
|
|
|
|
|
{'name': 'trend_volume_spike', 'type': 'float', 'min': 1.0, 'max': 3.0, 'strategy': 'trend_thresholds'},
|
|
|
|
|
{'name': 'trend_oversold', 'type': 'int', 'min': 20, 'max': 40, 'strategy': 'trend_thresholds'},
|
|
|
|
|
{'name': 'trend_overbought', 'type': 'int', 'min': 60, 'max': 80, 'strategy': 'trend_thresholds'},
|
|
|
|
|
# Confidence Thresholds
|
|
|
|
|
{'name': 'confidence_high', 'type': 'float', 'min': 0.5, 'max': 0.9, 'strategy': 'confidence'},
|
|
|
|
|
{'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",
|
|
|
|
|
"BollingerStrategy": "bollinger",
|
|
|
|
|
"MeanReversionStrategy": "mean_reversion",
|
|
|
|
|
"MomentumBreakoutStrategy": "momentum_breakout",
|
|
|
|
|
"MACDStrategy": "macd",
|
|
|
|
|
"KDJStrategy": "kdj",
|
|
|
|
|
"TurtleStrategy": "turtle",
|
|
|
|
|
"DailyBreakoutStrategy": "daily_breakout",
|
|
|
|
|
"WaveTheoryStrategy": "wave_theory",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_individual(individual):
|
|
|
|
|
"""解析个体基因为命名参数字典,并裁剪到定义范围"""
|
|
|
|
|
parsed = {}
|
|
|
|
|
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
|
|
|
|
|
val = individual[idx]
|
|
|
|
|
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
|
|
|
|
|
return parsed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_strategy_params(parsed: dict) -> dict:
|
|
|
|
|
"""从解析后的基因提取策略参数字典"""
|
|
|
|
|
strategy_params = {}
|
|
|
|
|
for param_def in PARAMETER_DEFINITIONS:
|
|
|
|
|
strategy_name = param_def.get('strategy')
|
|
|
|
|
if strategy_name in ('weight', 'signal', 'risk', 'market_state',
|
|
|
|
|
'trend_weights', 'trend_thresholds', 'confidence'):
|
|
|
|
|
continue
|
|
|
|
|
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'
|
|
|
|
|
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'
|
|
|
|
|
|
|
|
|
|
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(),使权重基因真正生效。
|
|
|
|
|
"""
|
|
|
|
|
weight_genes = {}
|
|
|
|
|
for param_def in PARAMETER_DEFINITIONS:
|
|
|
|
|
if param_def.get('strategy') != 'weight':
|
|
|
|
|
continue
|
|
|
|
|
class_name = param_def['name'].replace('weight_', '')
|
|
|
|
|
config_key = _class_name_to_config_key.get(class_name, class_name.lower())
|
|
|
|
|
weight_genes[config_key] = parsed[param_def['name']]
|
|
|
|
|
return weight_genes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _precompute_h1_states(multi_tf: MultiTimeframeDataStore, analyzer: MarketStateAnalyzer) -> list:
|
|
|
|
|
"""预计算H1市场状态序列 → 映射到M1时间轴"""
|
|
|
|
|
h1_df = multi_tf.ensure_timeframe(PERIOD_H1)
|
|
|
|
|
if h1_df is None or len(h1_df) == 0:
|
|
|
|
|
return [("none", 0.0)] * multi_tf.length
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
start = max(0, i - lookback)
|
|
|
|
|
df_slice = h1_df.iloc[start:i + 1]
|
|
|
|
|
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))
|
|
|
|
|
continue
|
|
|
|
|
while h1_pos < len(h1_index) - 1 and m1_time >= h1_index[h1_pos + 1]:
|
|
|
|
|
h1_pos += 1
|
|
|
|
|
if h1_pos < len(h1_states):
|
|
|
|
|
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:
|
|
|
|
|
"""★ 重写的适应度函数 — 所有权重基因、策略参数和风控参数真正生效
|
|
|
|
|
|
|
|
|
|
三个关键基因全部生效:
|
|
|
|
|
1. 策略参数 → 重新实例化策略并 run_backtest → 影响信号序列
|
|
|
|
|
2. 权重基因 → per-bar get_strategy_weights(individual_weights=...) → 影响信号组合
|
|
|
|
|
3. 风控参数 → PositionManager 在回测中真正使用
|
|
|
|
|
"""
|
|
|
|
|
parsed = _parse_individual(individual)
|
|
|
|
|
|
|
|
|
|
# 1. 提取策略参数
|
|
|
|
|
strategy_params = _extract_strategy_params(parsed)
|
|
|
|
|
|
|
|
|
|
# 2. ★ 提取权重基因(核心修复)
|
|
|
|
|
weight_genes = _extract_weight_genes(parsed)
|
|
|
|
|
|
|
|
|
|
# 3. 提取风控参数
|
|
|
|
|
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)),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 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)),
|
|
|
|
|
'hourly_data_count': 100,
|
|
|
|
|
}
|
|
|
|
|
trend_weights = {
|
|
|
|
|
'price_breakout': parsed.get('trend_price_breakout_weight', 0.35),
|
|
|
|
|
'volume_confirmation': parsed.get('trend_volume_confirmation_weight', 0.25),
|
|
|
|
|
'momentum oscillator': parsed.get('trend_momentum_oscillator_weight', 0.20),
|
|
|
|
|
'moving_average': parsed.get('trend_moving_average_weight', 0.20),
|
|
|
|
|
}
|
|
|
|
|
trend_thresholds = {
|
|
|
|
|
'strong_trend': parsed.get('trend_strong_threshold', 0.6),
|
|
|
|
|
'weak_trend': parsed.get('trend_weak_threshold', 0.3),
|
|
|
|
|
'volume_spike': parsed.get('trend_volume_spike', 1.5),
|
|
|
|
|
'oversold': parsed.get('trend_oversold', 30),
|
|
|
|
|
'overbought': parsed.get('trend_overbought', 70),
|
|
|
|
|
}
|
|
|
|
|
confidence_thresholds = {
|
|
|
|
|
'high_confidence': parsed.get('confidence_high', 0.7),
|
|
|
|
|
'medium_confidence': parsed.get('confidence_medium', 0.4),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 5. 构建 MarketStateAnalyzer 并预计算H1状态
|
|
|
|
|
analyzer = MarketStateAnalyzer(
|
|
|
|
|
timeframe=PERIOD_H1,
|
|
|
|
|
market_state_params=market_params,
|
|
|
|
|
trend_weights=trend_weights,
|
|
|
|
|
trend_thresholds=trend_thresholds,
|
|
|
|
|
confidence_thresholds=confidence_thresholds,
|
|
|
|
|
)
|
|
|
|
|
analyzer._precomputed_states = _precompute_h1_states(multi_tf, analyzer)
|
|
|
|
|
|
|
|
|
|
# 5.5. ★ 使用个体的策略参数重新实例化策略并重新计算信号
|
|
|
|
|
# 将 class_name → {params} 映射为 config_key → {params}
|
|
|
|
|
strategy_params_by_key = {
|
|
|
|
|
_class_name_to_config_key.get(cn, cn.lower()): params
|
|
|
|
|
for cn, params in strategy_params.items()
|
|
|
|
|
}
|
|
|
|
|
registry = StrategyRegistry()
|
|
|
|
|
sig_instances = registry.instantiate_all(SYMBOL, TIMEFRAME, strategy_params_by_key)
|
|
|
|
|
signals_df = pd.DataFrame(index=multi_tf.main_df.index)
|
|
|
|
|
for config_key, strat in sig_instances.items():
|
|
|
|
|
try:
|
|
|
|
|
sig = strat.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)
|
|
|
|
|
|
|
|
|
|
# 6. 回测环境
|
|
|
|
|
data_provider = BacktestDataProvider(multi_tf, INITIAL_CAPITAL)
|
|
|
|
|
pm = PositionManager(data_provider, trade_direction="both", risk_config=risk_params,
|
|
|
|
|
persist_peaks=False)
|
|
|
|
|
|
|
|
|
|
buy_th = parsed.get('buy_threshold', 1.5)
|
|
|
|
|
sell_th = parsed.get('sell_threshold', -1.5)
|
|
|
|
|
total_bars = multi_tf.length
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 组合当前 bar 的信号(使用个体参数重新计算的信号)
|
|
|
|
|
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:
|
|
|
|
|
pm.open_position("sell", current_price, 1.0, dry_run=True)
|
|
|
|
|
|
|
|
|
|
pm.monitor_positions(current_price, dry_run=True)
|
|
|
|
|
data_provider.tick()
|
|
|
|
|
|
|
|
|
|
# 8. 适应度 = 总盈亏
|
|
|
|
|
total_pnl = pm.total_equity - INITIAL_CAPITAL
|
|
|
|
|
return (total_pnl,)
|
|
|
|
|
|
|
|
|
|
|
2026-05-14 13:33:23 +08:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 12:00:45 +08:00
|
|
|
def load_historical_data():
|
|
|
|
|
"""一次性加载M1数据并返回 MultiTimeframeDataStore"""
|
2026-05-14 13:33:23 +08:00
|
|
|
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()
|
2026-05-11 12:00:45 +08:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
return signals_df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_optimizer():
|
|
|
|
|
"""运行遗传算法优化(所有基因真正生效)"""
|
|
|
|
|
try:
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
toolbox.register("map", pool.map)
|
|
|
|
|
else:
|
|
|
|
|
toolbox.register("map", map)
|
|
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
|
|
# 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"]
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
import traceback
|
|
|
|
|
logger.error(f"优化器错误: {e}\n{traceback.format_exc()}")
|
|
|
|
|
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()
|