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:
silencesdg
2026-05-21 20:26:55 +08:00
parent 218efef538
commit e1691c3c41
14 changed files with 699 additions and 449 deletions
+132 -282
View File
@@ -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()
+51 -18
View File
@@ -3,12 +3,12 @@ import signal
import sys
from datetime import datetime
from logger import logger
from config import SYMBOL, TIMEFRAME, REALTIME_CONFIG, SIGNAL_THRESHOLDS, RISK_CONFIG, RISK_CONFIG_CONST
import config
from core.risk import RiskController
from execution.weights import DynamicWeightManager
class RealtimeTrader:
"""实时交易器 (已重构为依赖注入)"""
"""实时交易器 (已重构为依赖注入) — ★ 每周期热加载配置"""
def __init__(self, data_provider, update_interval=60):
self.data_provider = data_provider
@@ -29,27 +29,28 @@ class RealtimeTrader:
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
# ★ 杠杆自适应:打印有效值
# ★ 打印风控参数(保证金%基准)
try:
acct = self.data_provider.get_account_info()
lev = acct.leverage if hasattr(acct, 'leverage') else acct.get('leverage', 2000)
except Exception:
lev = 2000
ratio = lev / 100.0
rl = config.RISK_CONFIG.get('risk_leverage', 100)
# ★ 启动参数一览
logger.info("=" * 50)
logger.info(f"品种: {SYMBOL} | 周期: M{TIMEFRAME} | 间隔: {self.update_interval}s | 杠杆: {lev}x")
logger.info(f"风控: 止损={RISK_CONFIG['stop_loss_pct']*ratio:.1%} | "
f"止盈={RISK_CONFIG['take_profit_pct']*ratio:.1%} | "
f"拖尾激活={RISK_CONFIG['min_profit_for_trailing']*ratio:.1%} | "
f"拖尾回撤={RISK_CONFIG['profit_retracement_pct']*ratio:.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(f"品种: {config.SYMBOL} | 周期: M{config.TIMEFRAME} | 间隔: {self.update_interval}s | 杠杆: {lev}x")
logger.info(f"风控: 止损={config.RISK_CONFIG['stop_loss_pct']:.0%} | "
f"止盈={config.RISK_CONFIG['take_profit_pct']:.0%} | "
f"拖尾激活={config.RISK_CONFIG['min_profit_for_trailing']:.0%} | "
f"拖尾回撤={config.RISK_CONFIG['profit_retracement_pct']:.0%}"
f" (基准={rl}x)")
logger.info(f"信号: 买入阈值={config.SIGNAL_THRESHOLDS.get('buy_threshold',1.5)} | "
f"卖出阈值={config.SIGNAL_THRESHOLDS.get('sell_threshold',-1.5)}")
logger.info(f"仓位: 最多多={config.REALTIME_CONFIG['max_long_positions']} 最多空={config.REALTIME_CONFIG['max_short_positions']} | "
f"超时平仓={'' if config.RISK_CONFIG_CONST.get('enable_time_based_exit',True) else ''}")
logger.info(f"对冲: 信号对冲={'' if config.REALTIME_CONFIG.get('hedge_enabled',False) else ''} | "
f"锁仓={'' if config.REALTIME_CONFIG.get('lock_enabled',False) else ''}")
logger.info("=" * 50)
return True
@@ -60,9 +61,13 @@ class RealtimeTrader:
def _run_cycle(self):
try:
self._cycle_count += 1
# ★ 热加载配置:cron 优化器改完 config.py 后自动生效
config.reload()
self.risk_controller.sync_state()
current_price = self.data_provider.get_current_price(SYMBOL)
current_price = self.data_provider.get_current_price(config.SYMBOL)
if not current_price:
return
@@ -75,8 +80,8 @@ class RealtimeTrader:
weights.append(weight)
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)
buy_threshold = config.SIGNAL_THRESHOLDS.get('buy_threshold', 1.5)
sell_threshold = config.SIGNAL_THRESHOLDS.get('sell_threshold', -1.5)
direction = None
if weighted_signal_sum > buy_threshold:
@@ -84,6 +89,33 @@ class RealtimeTrader:
elif weighted_signal_sum < sell_threshold:
direction = "sell"
# ★ 同向门槛递增:已有N单同向时,第N+1单需要更强信号
if direction:
# ★ 适应度门槛:回测亏钱就不开新单
last_fitness = getattr(config, 'LAST_OPTIMIZATION_FITNESS', 0)
min_fitness = config.RISK_CONFIG.get('min_backtest_fitness', 250)
if last_fitness < min_fitness:
if self._cycle_count % 30 == 0:
logger.warning(f"⚠️ 回测适应度{last_fitness:.0f}<{min_fitness},暂停开仓(监控持仓中)")
direction = None
if direction:
alpha = config.RISK_CONFIG.get('entry_escalation_alpha', 1.2)
pm = self.risk_controller.position_manager
n_long = sum(1 for p in pm.positions if p['position_type'] == 'long')
n_short = sum(1 for p in pm.positions if p['position_type'] == 'short')
if direction == 'buy':
adjusted = buy_threshold * (alpha ** n_long)
if weighted_signal_sum <= adjusted:
logger.debug(f"BUY信号{weighted_signal_sum:.2f}<调整阈值{adjusted:.2f}(已有{n_long}多单), 忽略")
direction = None
elif direction == 'sell':
adjusted = sell_threshold * (alpha ** n_short)
if weighted_signal_sum >= adjusted:
logger.debug(f"SELL信号{weighted_signal_sum:.2f}>调整阈值{adjusted:.2f}(已有{n_short}空单), 忽略")
direction = None
# 只在信号触发时打印决策依据
if direction:
logger.info(f"⚡ 信号触发 | 加权={weighted_signal_sum:.2f} | "
@@ -124,6 +156,7 @@ class RealtimeTrader:
self.running = False
try:
if self.risk_controller:
self.risk_controller.position_manager.cleanup_peak_data()
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()
+4 -3
View File
@@ -10,7 +10,7 @@ import strategies # noqa: F401 — 触发策略注册
from logger import logger
from core.risk.market_state import MarketStateAnalyzer
from core.signal.registry import StrategyRegistry
from config import SYMBOL, TIMEFRAME
import config
class DynamicWeightManager:
@@ -27,7 +27,7 @@ class DynamicWeightManager:
def _ensure_strategies(self):
if not self._strategies_initialized:
self.registry.instantiate_all(SYMBOL, TIMEFRAME,
self.registry.instantiate_all(config.SYMBOL, config.TIMEFRAME,
data_provider=self.data_provider)
self._strategies_initialized = True
@@ -46,7 +46,8 @@ class DynamicWeightManager:
return result
def get_current_weights(self) -> dict:
"""获取当前实时权重"""
"""获取当前实时权重 — ★ 热加载配置"""
config.reload()
market_state, confidence = self.analyzer.get_market_state()
return self.analyzer.get_strategy_weights(market_state, confidence)