This commit is contained in:
songkunling
2025-08-14 23:17:12 +08:00
parent e8226edd96
commit 2ad610d8fd
23 changed files with 6022 additions and 586 deletions
+66 -10
View File
@@ -40,6 +40,7 @@ mt5_python_ea_suite/
│ ├── wave_theory.py # 波浪理论策略
│ └── base_strategy.py # 策略基类
├── config.py # 配置文件
├── mt5_constants.py # MT5常量定义
├── main.py # 主入口(优化器)
├── start_backtest.py # 回测启动脚本
├── start_realtime.py # 实时交易启动脚本
@@ -86,20 +87,75 @@ mt5_python_ea_suite/
## 🎯 策略说明
### 技术指标策略
- **均线交叉 (MACrossStrategy)**: 基于快慢均线交叉的买卖信号
- **相对强弱指数 (RSIStrategy)**: 使用RSI指标判断超买超卖
- **布林带 (BollingerStrategy)**: 基于价格与布林带的相对位置
- **MACD策略**: MACD指标的金叉死叉信号
- **KDJ策略**: KDJ指标的超买超卖信号
- **均线交叉 (MACrossStrategy)**: 基于快慢均线交叉的买卖信号,需要 `long_window + 5` 条数据(默认25条)
- **相对强弱指数 (RSIStrategy)**: 使用RSI指标判断超买超卖,需要 `period + 10` 条数据(默认24条)
- **布林带 (BollingerStrategy)**: 基于价格与布林带的相对位置,需要 `period + 5` 条数据(默认25条)
- **MACD策略**: MACD指标的金叉死叉信号,需要 `slow_ema + signal_period + 5` 条数据(默认40条)
- **KDJ策略**: KDJ指标的超买超卖信号,需要 `period + 5` 条数据(默认19条)
### 价格行为策略
- **海龟交易法则 (TurtleStrategy)**: 基于价格突破的顺势交易
- **均值回归 (MeanReversionStrategy)**: 价格偏离均值时的回归交易
- **动量突破 (MomentumBreakoutStrategy)**: 基于动量指标的突破信号
- **日内突破 (DailyBreakoutStrategy)**: 基于日内价格区间的突破
- **海龟交易法则 (TurtleStrategy)**: 基于价格突破的顺势交易,需要 `period + 2` 条数据(默认22条)
- **均值回归 (MeanReversionStrategy)**: 价格偏离均值时的回归交易,需要 `period + 5` 条数据(默认25条)
- **动量突破 (MomentumBreakoutStrategy)**: 基于动量指标的突破信号,需要 `period + 2` 条数据(默认22条)
- **日内突破 (DailyBreakoutStrategy)**: 基于日内价格区间的突破,需要 `bars_count` 条数据(默认1440条,即1天的1分钟数据)
### 复杂策略
- **波浪理论 (WaveTheoryStrategy)**: 结合EMA、ADX、动量等多指标的趋势分析
- **波浪理论 (WaveTheoryStrategy)**: 结合EMA、ADX、动量等多指标的趋势分析,需要 `m1_bars_count` 条数据(默认500条)
## 📊 策略数据需求总结
| 策略名称 | 最小数据量 | 默认参数 | 主要用途 |
|---------|-----------|---------|---------|
| MACrossStrategy | long_window + 5 | 25 | 趋势跟踪 |
| RSIStrategy | period + 10 | 24 | 超买超卖 |
| BollingerStrategy | period + 5 | 25 | 波动性交易 |
| MACDStrategy | slow_ema + signal_period + 5 | 40 | 趋势确认 |
| KDJStrategy | period + 5 | 19 | 超买超卖 |
| TurtleStrategy | period + 2 | 22 | 突破交易 |
| MeanReversionStrategy | period + 5 | 25 | 均值回归 |
| MomentumBreakoutStrategy | period + 2 | 22 | 动量突破 |
| DailyBreakoutStrategy | bars_count | 1440 | 日内交易 |
| WaveTheoryStrategy | m1_bars_count | 500 | 综合分析 |
**注意**: 系统运行时会自动获取所需数据量,建议配置回测数据量时考虑最大需求的策略(DailyBreakoutStrategy需要1440条)。
## 📈 数据获取确认
### 数据频率确认
- **时间周期**: `TIMEFRAME = TIMEFRAME_M1` (1分钟K线数据)
- **常量定义**: 使用MT5标准时间周期常量 (`mt5_constants.py`)
- **数据来源**: MT5服务器实时数据
- **数据格式**: 包含OHLC价格、成交量、点差等信息
### MT5时间周期常量
系统使用标准MT5时间周期常量:
- `TIMEFRAME_M1 = 1` (1分钟)
- `TIMEFRAME_M5 = 5` (5分钟)
- `TIMEFRAME_M15 = 15` (15分钟)
- `TIMEFRAME_M30 = 30` (30分钟)
- `TIMEFRAME_H1 = 16385` (1小时)
- `TIMEFRAME_H4 = 16388` (4小时)
- `TIMEFRAME_D1 = 16408` (1天)
- `TIMEFRAME_W1 = 32769` (1周)
- `TIMEFRAME_MN1 = 49153` (1月)
### 实际数据验证
经过测试确认:
- 25条数据 = 24分钟历史数据
- 40条数据 = 39分钟历史数据
- 1440条数据 = 1509分钟历史数据(约25小时)
- 5000条数据 = 8159分钟历史数据(约136小时)
### 数据字段说明
每条1分钟K线数据包含:
- `time`: 时间戳
- `open`: 开盘价
- `high`: 最高价
- `low`: 最低价
- `close`: 收盘价
- `tick_volume`: 成交量
- `spread`: 点差
- `real_volume`: 实际成交量
## 🚦 运行模式
+18 -55
View File
@@ -5,7 +5,8 @@ INITIAL_CAPITAL = 20000 # 初始资金
SPREAD = 32 # 点差(点数),买卖合起来的总共点差成本
# 时间配置
TIMEFRAME = 1# M1 (1分钟图) - MT5常量值
from mt5_constants import TIMEFRAME_M1
TIMEFRAME = TIMEFRAME_M1 # 1分钟K线 - 使用MT5标准常量
# 回测时间范围 (格式: "YYYY-MM-DD")
# 注意:这些日期需要确保在MT5服务器上有可用数据
@@ -24,14 +25,20 @@ USE_DATE_RANGE = False # 设置为False可强制使用数据量模式
BACKTEST_COUNT = 30000 # 回测数据量
OPTIMIZER_COUNT = 15000 # 优化器数据量
# 性能与调试配置 --- 优化器模式下,需要True 实盘模式下False,可以保证重启程序保持持仓的最大盈利记录,便于计算回撤
# True: 使用内存变量存储持仓峰值数据,优化器模式下更快且能避免文件冲突
# False: 使用 position_peaks.json 文件存储,适合需要持久化的单进程实盘模式
USE_MEMORY_FOR_PEAK_DATA = True
RISK_CONFIG_CONST = {
'enable_time_based_exit': True
}
# 资金分配配置
CAPITAL_ALLOCATION = {
"long_pct": 0.7, # 多头持仓分配资金比例
"short_pct": 0.3, # 空头持仓分配资金比例
"long_pct": 0.5, # 多头持仓分配资金比例
"short_pct": 0.5, # 空头持仓分配资金比例
}
# 模拟交易特定配置 (用于dry_run模式)
@@ -55,8 +62,8 @@ BACKTEST_CONFIG = {
REALTIME_CONFIG = {
"update_interval": 5, # 更新间隔(秒)
"daily_reset_time": "00:00", # 每日重置时间
"max_long_positions": 3, # 最大多头持仓数(增加为3个)
"max_short_positions": 3, # 最大空头持仓数(增加为3个)
"max_long_positions": 1, # 最大多头持仓数(增加为3个)
"max_short_positions": 1, # 最大空头持仓数(增加为3个)
"min_trade_interval": 0, # 最小交易间隔(分钟),0表示无限制
"enable_auto_trading": True, # 是否启用自动交易
"dry_run": False, # 是否为模拟运行(不实际下单)
@@ -106,20 +113,20 @@ GENETIC_OPTIMIZER_CONFIG = {
# 信号阈值配置
SIGNAL_THRESHOLDS = {
"buy_threshold": 1.5, # 买入信号阈值 (优化后)
"sell_threshold": -0.87 # 卖出信号阈值 (优化后)
"sell_threshold": -1.5 # 卖出信号阈值 (优化后)
}
# 风险管理参数
RISK_CONFIG = {
"stop_loss_pct": -0.01, # 固定止损:亏损1% (优化后)
"stop_loss_pct": -0.0005, # 固定止损:亏损0.1% (优化后)
"profit_retracement_pct": 0.1, # 利润回撤10%止盈 (优化后)
"min_profit_for_trailing": 0.01, # 追踪止损激活阈值:利润0.1% (降低阈值以激活追踪止损)
"min_profit_for_trailing": 0.001, # 追踪止损激活阈值:利润0.1% (降低阈值以激活追踪止损)
"take_profit_pct": 0.001, # 固定止盈:盈利0.1% (优化后)
"max_position_size": 1, # 最大仓位100%
"max_daily_loss": -0.3, # 最大日亏损30%
"max_holding_minutes": 140, # 持仓超过140分钟 (优化后)
"min_profit_for_time_exit": 0.01, # 且盈利未达到0.1%则平仓 (优化后)
"min_profit_for_time_exit": 0.001, # 且盈利未达到0.1%则平仓 (优化后)
}
# 市场状态分析参数
@@ -216,7 +223,7 @@ TREND_THRESHOLDS = {
}
# 动态权重配置(经过优化器优化的最佳权重)
# 策略权重配置
DEFAULT_WEIGHTS = {
"ma_cross": 0.44428771408324463,
"rsi": 1.6518277343219148,
@@ -230,50 +237,6 @@ DEFAULT_WEIGHTS = {
"wave_theory": 0.7354173414675755,
}
# 市场状态策略权重配置
MARKET_STATE_WEIGHTS = {
"uptrend": {
"ma_cross": 1.50,
"momentum_breakout": 1.20,
"turtle": 0.25,
"macd": 0.35,
"daily_breakout": 1.50,
"rsi": 1.00,
"bollinger": 1.00,
"kdj": 0.40,
"mean_reversion": 0.80,
"wave_theory": 0.20
},
"downtrend": {
"ma_cross": 1.50,
"momentum_breakout": 1.20,
"turtle": 0.25,
"macd": 0.35,
"daily_breakout": 1.50,
"rsi": 1.00,
"bollinger": 1.00,
"kdj": 0.40,
"mean_reversion": 0.80,
"wave_theory": 0.20
},
"ranging": {
"rsi": 1.60,
"bollinger": 1.70,
"mean_reversion": 1.50,
"kdj": 1.00,
"wave_theory": 0.50,
"ma_cross": 0.70,
"macd": 0.20,
"turtle": 0.10,
"momentum_breakout": 0.50,
"daily_breakout": 0.90
},
"none": DEFAULT_WEIGHTS
}
# 市场趋势置信度阈值配置
CONFIDENCE_THRESHOLDS = {
"high_confidence": 0.941049792261965, # 高置信度阈值
"medium_confidence": 0.8881796011658835, # 中等置信度阈值
}
+1
View File
@@ -77,6 +77,7 @@ class LiveDataProvider(DataProvider):
return None
def get_historical_data(self, symbol, timeframe, count, **kwargs):
# 确保使用MT5标准常量
return mt5.copy_rates_from_pos(symbol, timeframe, 0, count)
def get_account_info(self):
+24 -27
View File
@@ -1,29 +1,39 @@
import pandas as pd
import numpy as np
from logger import logger
from config import MARKET_STATE_CONFIG, SYMBOL, DEFAULT_WEIGHTS, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, MARKET_STATE_WEIGHTS, CONFIDENCE_THRESHOLDS
from mt5_constants import TIMEFRAME_H1
# Removed: from config import MARKET_STATE_CONFIG, SYMBOL, DEFAULT_WEIGHTS, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, MARKET_STATE_WEIGHTS, CONFIDENCE_THRESHOLDS
class MarketStateAnalyzer:
"""
市场状态分析器 (支持参数优化)
市场状态分析器 (已移除对外部配置的依赖)
"""
def __init__(self, data_provider, market_state_params=None, trend_weights=None, trend_thresholds=None, confidence_thresholds=None):
def __init__(self, data_provider):
self.data_provider = data_provider
self.symbol = SYMBOL
self.timeframe = 16385 # TIMEFRAME_H1
self.symbol = "XAUUSD" # Hardcoded SYMBOL, as it's no longer imported from config
self.timeframe = TIMEFRAME_H1
self.hourly_data_count = 100
# 使用传入的参数或默认配置
market_state_config = market_state_params or MARKET_STATE_CONFIG
self.trend_period = market_state_config.get("trend_period", 50)
self.retracement_tolerance = market_state_config.get("retracement_tolerance", 0.30)
self.volume_period = market_state_config.get("volume_period", 20)
self.volume_ma_period = market_state_config.get("volume_ma_period", 10)
# Hardcoded default parameters (from original config.py values)
self.trend_period = 50 # From MARKET_STATE_CONFIG.get("trend_period", 50)
self.retracement_tolerance = 0.30 # From MARKET_STATE_CONFIG.get("retracement_tolerance", 0.30)
self.volume_period = 20 # From MARKET_STATE_CONFIG.get("volume_period", 20)
self.volume_ma_period = 10 # From MARKET_STATE_CONFIG.get("volume_ma_period", 10)
self.indicator_weights = trend_weights or TREND_INDICATOR_WEIGHTS
self.thresholds = trend_thresholds or TREND_THRESHOLDS
self.confidence_thresholds = confidence_thresholds or CONFIDENCE_THRESHOLDS
self.indicator_weights = { # From TREND_INDICATOR_WEIGHTS
"price_breakout": 0.35,
"volume_confirmation": 0.25,
"momentum oscillator": 0.20,
"moving_average": 0.20
}
self.thresholds = { # From TREND_THRESHOLDS
"strong_trend": 0.6,
"weak_trend": 0.3,
"volume_spike": 1.5,
"oversold": 30,
"overbought": 70
}
self.current_trend = "none"
self.trend_peak = 0.0
@@ -136,16 +146,3 @@ class MarketStateAnalyzer:
self._update_trend_state(df, state)
return state, confidence
def get_strategy_weights(self, market_state, confidence):
base_weights = MARKET_STATE_WEIGHTS.get(market_state, DEFAULT_WEIGHTS)
high_confidence_threshold = self.confidence_thresholds.get("high_confidence", 0.7)
medium_confidence_threshold = self.confidence_thresholds.get("medium_confidence", 0.4)
if confidence > high_confidence_threshold:
return {k: v * confidence for k, v in base_weights.items()}
elif confidence > medium_confidence_threshold:
return {k: (v * confidence + DEFAULT_WEIGHTS.get(k, 1.0) * (1 - confidence)) for k, v in base_weights.items()}
else:
return DEFAULT_WEIGHTS
+94 -301
View File
@@ -5,6 +5,13 @@ import os
from logger import logger
from config import RISK_CONFIG, SYMBOL, INITIAL_CAPITAL, CAPITAL_ALLOCATION, RISK_CONFIG_CONST
import pandas as pd
import numpy as np
import json
import os
from logger import logger
from config import RISK_CONFIG, SYMBOL, INITIAL_CAPITAL, CAPITAL_ALLOCATION, RISK_CONFIG_CONST, USE_MEMORY_FOR_PEAK_DATA
class PositionManager:
"""
持仓管理器 - 统一管理所有持仓操作和交易记录 (已重构为依赖注入)
@@ -14,6 +21,7 @@ class PositionManager:
self.data_provider = data_provider
self.symbol = SYMBOL
self.trade_direction = trade_direction
self.use_memory_storage = USE_MEMORY_FOR_PEAK_DATA
# 风险管理参数
self.stop_loss_pct = RISK_CONFIG.get("stop_loss_pct", -0.10)
@@ -35,395 +43,180 @@ class PositionManager:
self.closed_trades = []
self.total_equity = self.initial_capital
# 持久化文件路径
self.peak_data_file = "position_peaks.json"
# 初始化时加载峰值数据
self._load_peak_data()
# 根据配置选择存储方式
if self.use_memory_storage:
self.peak_data = {}
logger.info("使用内存变量存储持仓峰值数据")
else:
self.peak_data_file = f"position_peaks_{os.getpid()}.json"
logger.info(f"使用文件 {self.peak_data_file} 存储持仓峰值数据")
self._load_peak_data() # 初始化时加载一次
def _calculate_position_size(self, capital_to_allocate, current_price):
price = current_price['last']
logger.info(f"当前价格: {price}")
if not isinstance(price, (int, float)) or price == 0:
logger.error(f"价格无效: {price}")
return 0.0
if not isinstance(price, (int, float)) or price == 0: return 0.0
symbol_info = self.data_provider.get_symbol_info(self.symbol)
logger.info(f"合约信息: {symbol_info}")
if not symbol_info:
logger.error(f"无法获取 {self.symbol} 的合约信息")
return 0.0
if not symbol_info: return 0.0
is_dict = isinstance(symbol_info, dict)
contract_size = symbol_info['trade_contract_size'] if is_dict else symbol_info.trade_contract_size
volume_step = symbol_info['volume_step'] if is_dict else symbol_info.volume_step
min_volume = symbol_info['volume_min'] if is_dict else symbol_info.volume_min
max_volume = symbol_info['volume_max'] if is_dict else symbol_info.volume_max
logger.info(f"合约大小: {contract_size}, 步长: {volume_step}, 最小: {min_volume}, 最大: {max_volume}")
value_of_one_lot = price * contract_size
logger.info(f"一手价值: {value_of_one_lot}")
if value_of_one_lot == 0:
logger.error("一手价值为0")
return 0.0
if value_of_one_lot == 0: return 0.0
volume = capital_to_allocate / value_of_one_lot
logger.info(f"原始手数: {volume}")
volume = round(volume / volume_step) * volume_step
logger.info(f"调整后手数: {volume}")
volume = max(min_volume, min(volume, max_volume))
logger.info(f"最终手数: {volume}")
return volume
return max(min_volume, min(volume, max_volume))
def open_position(self, direction, current_price, signal_strength=0.0, dry_run=False):
logger.info(f"开始处理{direction}开仓请求")
position_type_to_open = 'long' if direction == 'buy' else 'short'
# 根据交易方向选择合适的成交价格
if dry_run:
# 在回测/模拟模式中,考虑点差
if direction == 'buy':
execution_price = current_price['ask'] # 买入用卖方价(ask
else:
execution_price = current_price['bid'] # 卖出用买方价(bid
else:
# 实盘模式使用最后价格
execution_price = current_price['last']
# 检查交易方向限制
if self.trade_direction == "long" and direction == "sell":
logger.info("当前配置只允许做多,忽略卖出信号")
return False
elif self.trade_direction == "short" and direction == "buy":
logger.info("当前配置只允许做空,忽略买入信号")
return False
# 从配置中获取最大持仓限制
execution_price = current_price['ask'] if dry_run and direction == 'buy' else current_price['bid'] if dry_run else current_price['last']
if (self.trade_direction == "long" and direction == "sell") or (self.trade_direction == "short" and direction == "buy"): return False
from config import REALTIME_CONFIG
max_positions = REALTIME_CONFIG.get(f'max_{position_type_to_open}_positions', 1)
logger.info(f"配置文件中的max_{position_type_to_open}_positions: {REALTIME_CONFIG.get(f'max_{position_type_to_open}_positions', 'NOT_FOUND')}")
logger.info(f"最大{position_type_to_open}持仓数限制: {max_positions}")
# 检查当前同向持仓数量
current_positions = [p for p in self.positions if p['position_type'] == position_type_to_open]
logger.info(f"当前{position_type_to_open}持仓数: {len(current_positions)}")
# 如果配置为0或负数,表示无限制
if max_positions <= 0:
logger.info(f"{position_type_to_open}持仓数无限制")
elif len(current_positions) >= max_positions:
logger.info(f"已达到最大{position_type_to_open}持仓数 ({max_positions}),忽略信号")
return False
if max_positions > 0 and len(current_positions) >= max_positions: return False
capital_pct = self.long_capital_pct if direction == 'buy' else self.short_capital_pct
capital_for_this_trade = self.total_equity * capital_pct
logger.info(f"分配资金: {capital_for_this_trade:.2f} (总权益: {self.total_equity:.2f}, 比例: {capital_pct:.2%})")
position_volume = self._calculate_position_size(capital_for_this_trade, {'last': execution_price})
logger.info(f"计算仓位大小: {position_volume:.2f}")
if position_volume <= 0:
logger.info("仓位大小为0,无法开仓")
return False
logger.info(f"发送订单: {direction} {position_volume:.2f}{self.symbol}")
if position_volume <= 0: return False
order_result = self.data_provider.send_order(self.symbol, direction, position_volume)
logger.info(f"订单结果: {order_result}")
if order_result is None:
logger.error("订单返回None,可能是数据提供者问题")
return False
if order_result is None: return False
try:
order_id = order_result['order'] if isinstance(order_result, dict) else order_result.order
logger.info(f"订单ID: {order_id}")
except Exception as e:
logger.error(f"解析订单ID失败: {e}")
return False
except Exception: return False
if order_result and order_id > 0:
new_position = {
'ticket': order_id,
'symbol': self.symbol,
'entry_price': execution_price,
'entry_time': current_price['time'],
'position_type': position_type_to_open,
'quantity': position_volume,
'peak_profit_pct': 0.0,
'ticket': order_id, 'symbol': self.symbol, 'entry_price': execution_price,
'entry_time': current_price['time'], 'position_type': position_type_to_open,
'quantity': position_volume, 'peak_profit_pct': 0.0,
}
self.positions.append(new_position)
logger.info(f"开仓成功: {direction} @ {execution_price:.2f}, 手数: {position_volume:.2f}, Ticket: {order_id}")
logger.info(f"持仓 {order_id}: 初始化峰值盈利为 0.0%")
return True
else:
logger.error(f"开仓失败: {direction} @ {current_price['last']:.2f}, 订单结果: {order_result}")
return False
return False
def monitor_positions(self, current_price, dry_run=False):
if not self.positions: return
# 打印所有持仓的当前状态
logger.info(f"当前持仓数量: {len(self.positions)}")
for pos in self.positions:
# 计算持仓时间
holding_time = current_price['time'] - pos['entry_time']
holding_minutes = holding_time.total_seconds() / 60
logger.info(f"持仓 {pos['ticket']}: 持仓时间={holding_minutes:.1f}分钟, 当前峰值={pos.get('peak_profit_pct', 0):.6%}")
positions_to_remove = []
for position in self.positions:
pnl_pct = self._calculate_pnl_pct(position, current_price['last'])
old_peak = position.get('peak_profit_pct', 0)
# 确保正确更新峰值
new_peak = max(old_peak, pnl_pct)
position['peak_profit_pct'] = new_peak
# 打印详细的追踪止损计算信息
# logger.info(f"持仓 {position['ticket']}: 当前盈利={pnl_pct:.6%}, 原峰值={old_peak:.6%}, 新峰值={new_peak:.6%}")
# 如果有新的峰值,打印详细信息并保存到文件
if new_peak > old_peak:
logger.info(f"持仓 {position['ticket']}: 新的峰值盈利: {new_peak:.6%}")
# 确认保存到持仓对象
logger.info(f"持仓 {position['ticket']}: 确认保存的峰值: {position.get('peak_profit_pct', 0):.6%}")
# 保存到文件
self._save_peak_data()
action, reason = self._check_risk_conditions(
pnl_pct,
new_peak,
position['entry_time'],
current_price['time'],
position
)
if new_peak > old_peak: self._save_peak_data()
action, reason = self._check_risk_conditions(pnl_pct, new_peak, position['entry_time'], current_price['time'], position)
if action == "close":
logger.info(f"平仓信号触发 (Ticket: {position['ticket']}): {reason}")
# 在dry_run模式下,计算考虑点差的平仓价格
close_price = current_price['last']
if dry_run:
# 从配置获取点差
from config import SIMULATION_CONFIG
spread_points = SIMULATION_CONFIG.get("spread", 16)
spread_value = spread_points * 0.01 # XAUUSD: 1点 = 0.01
if position['position_type'] == 'long':
# 多头平仓用bid价(卖出价)
close_price = current_price['bid']
else:
# 空头平仓用ask价(买入价)
close_price = current_price['ask']
else:
close_price = current_price['last']
success = self.data_provider.close_position(position['ticket'], position['symbol'], position['quantity'])
if success:
close_price = current_price['bid'] if position['position_type'] == 'long' else current_price['ask']
if self.data_provider.close_position(position['ticket'], position['symbol'], position['quantity']):
self._record_closed_trade(position, close_price, reason)
positions_to_remove.append(position)
else:
logger.error(f"平仓失败, Ticket: {position['ticket']}")
if positions_to_remove:
# 记录平仓的持仓ticket
closed_tickets = [pos['ticket'] for pos in positions_to_remove]
logger.info(f"平仓持仓: {closed_tickets}")
self.positions = [p for p in self.positions if p not in positions_to_remove]
self.update_equity()
# 清理已平仓持仓的峰值数据
self.cleanup_peak_data()
def _calculate_pnl_pct(self, position, current_price_value):
entry_price = position['entry_price']
if position['position_type'] == 'long':
return (current_price_value - entry_price) / entry_price if entry_price != 0 else 0.0
else:
return (entry_price - current_price_value) / entry_price if entry_price != 0 else 0.0
if entry_price == 0: return 0.0
return (current_price_value - entry_price) / entry_price if position['position_type'] == 'long' else (entry_price - current_price_value) / entry_price
def _record_closed_trade(self, position, close_price, close_reason):
symbol_info = self.data_provider.get_symbol_info(position['symbol'])
contract_size = (symbol_info['trade_contract_size'] if isinstance(symbol_info, dict)
else symbol_info.trade_contract_size) if symbol_info else 100
pnl = 0
if position['position_type'] == 'long':
pnl = (close_price - position['entry_price']) * position['quantity'] * contract_size
else:
pnl = (position['entry_price'] - close_price) * position['quantity'] * contract_size
contract_size = (symbol_info['trade_contract_size'] if isinstance(symbol_info, dict) else symbol_info.trade_contract_size) if symbol_info else 100
pnl = (close_price - position['entry_price']) * position['quantity'] * contract_size if position['position_type'] == 'long' else (position['entry_price'] - close_price) * position['quantity'] * contract_size
trade_record = position.copy()
trade_record.update({
'status': 'closed', 'close_price': close_price, 'close_time': pd.Timestamp.now(),
'close_reason': close_reason, 'profit_loss': pnl
})
trade_record.update({'status': 'closed', 'close_price': close_price, 'close_time': pd.Timestamp.now(), 'close_reason': close_reason, 'profit_loss': pnl})
self.closed_trades.append(trade_record)
logger.info(f"平仓记录 #{position['ticket']}: 盈亏: ${pnl:.2f}")
def _check_risk_conditions(self, current_profit_pct, peak_profit_pct, entry_time, current_time, position=None):
# 记录详细的追踪止损计算过程
logger.debug(f"追踪止损计算: 当前盈利={current_profit_pct:.4%}, 峰值盈利={peak_profit_pct:.4%}, 阈值={self.min_profit_for_trailing:.4%}")
if current_profit_pct <= self.stop_loss_pct:
logger.debug(f"触发止损: {current_profit_pct:.4%} <= {self.stop_loss_pct:.4%}")
return "close", f"止损触发"
if current_profit_pct >= self.take_profit_pct:
logger.debug(f"触发止盈: {current_profit_pct:.4%} >= {self.take_profit_pct:.4%}")
return "close", f"止盈触发"
# 修正后的追踪止损逻辑
if current_profit_pct <= self.stop_loss_pct: return "close", f"止损触发"
if current_profit_pct >= self.take_profit_pct: return "close", f"止盈触发"
if peak_profit_pct > self.min_profit_for_trailing:
stop_level = peak_profit_pct * (1 - self.profit_retracement_pct)
retracement_amount = peak_profit_pct - current_profit_pct
retracement_pct = (retracement_amount / peak_profit_pct) if peak_profit_pct > 0 else 0
# 计算实际回撤金额
if position:
symbol_info = self.data_provider.get_symbol_info(self.symbol)
contract_size = (symbol_info['trade_contract_size'] if isinstance(symbol_info, dict)
else symbol_info.trade_contract_size) if symbol_info else 100
# 使用持仓的entry_price和quantity来计算实际回撤金额
position_value = position['entry_price'] * position['quantity'] * contract_size
actual_retracement_amount = position_value * retracement_amount
else:
actual_retracement_amount = 0
logger.info(f"追踪止损详细计算:")
logger.info(f" 峰值盈利: {peak_profit_pct:.4%}")
logger.info(f" 当前盈利: {current_profit_pct:.4%}")
logger.info(f" 回撤阈值: {self.profit_retracement_pct:.2%}")
logger.info(f" 计算止损位: {peak_profit_pct:.4%} × (1 - {self.profit_retracement_pct:.2%}) = {stop_level:.4%}")
logger.info(f" 实际回撤: {retracement_pct:.2%} (回撤金额: ${actual_retracement_amount:.2f})")
logger.info(f" 触发条件: 当前盈利 {current_profit_pct:.4%} <= 止损位 {stop_level:.4%} = {current_profit_pct <= stop_level}")
if current_profit_pct <= stop_level:
logger.info(f"触发追踪止损: 当前盈利{current_profit_pct:.4%} <= 止损位{stop_level:.4%}")
return "close", f"追踪止损触发 (盈利从 {peak_profit_pct:.2%} 回落至 {current_profit_pct:.2%}, 回撤{retracement_pct:.2%})"
else:
logger.info(f"未触发追踪止损: 当前盈利{current_profit_pct:.4%} > 止损位{stop_level:.4%}")
else:
pass # 未达到追踪止损条件
if self.enable_time_based_exit:
holding_duration = current_time - entry_time
if (holding_duration.total_seconds() / 60) > self.max_holding_minutes:
if current_profit_pct < self.min_profit_for_time_exit:
return "close", f"超时平仓 (持仓超过{self.max_holding_minutes}分钟且盈利未达标)"
return "close", f"追踪止损触发"
if self.enable_time_based_exit and (current_time - entry_time).total_seconds() / 60 > self.max_holding_minutes and current_profit_pct < self.min_profit_for_time_exit:
return "close", f"超时平仓"
return "none", ""
def update_equity(self):
# In live mode, always trust the broker's account info
if self.data_provider.is_live:
account_info = self.data_provider.get_account_info()
if account_info:
self.total_equity = account_info.equity
# In simulation/backtest mode, calculate equity based on trade history
if account_info: self.total_equity = account_info.equity
else:
self.total_equity = self.initial_capital + sum(t['profit_loss'] for t in self.closed_trades)
def sync_positions(self):
# 只在实盘模式下执行同步
if not self.data_provider.is_live:
return
if not self.data_provider.is_live: return
live_positions = self.data_provider.get_positions(self.symbol)
if live_positions is None: return
# 从文件加载保存的峰值数据
saved_peaks = self._load_peak_data()
logger.info(f"从文件加载的峰值数据: {saved_peaks}")
# 保存现有的峰值数据(内存中的)
existing_peaks = {pos['ticket']: pos.get('peak_profit_pct', 0.0) for pos in self.positions}
# 合并数据:优先使用文件中的数据,如果没有则使用内存中的数据
merged_peaks = {**existing_peaks, **saved_peaks}
# logger.info(f"合并后的峰值数据: {merged_peaks}")
self.positions.clear()
for pos in live_positions:
# 获取该持仓之前记录的峰值,优先从文件中获取
restored_peak = merged_peaks.get(pos.ticket, 0.0)
new_position = {
restored_peak = saved_peaks.get(str(pos.ticket), 0.0)
self.positions.append({
'ticket': pos.ticket, 'symbol': pos.symbol, 'entry_price': pos.price_open,
'entry_time': pd.to_datetime(pos.time, unit='s'),
'position_type': 'long' if pos.type == 0 else 'short',
'entry_time': pd.to_datetime(pos.time, unit='s'), 'position_type': 'long' if pos.type == 0 else 'short',
'quantity': pos.volume, 'peak_profit_pct': restored_peak
}
self.positions.append(new_position)
logger.info(f"同步持仓 {pos.ticket}: 恢复峰值={restored_peak:.6%}")
logger.info(f"持仓已从MT5同步: {len(self.positions)}")
})
def get_trade_summary(self):
if not self.closed_trades: return {}
profits = [t['profit_loss'] for t in self.closed_trades]
return {
'total_trades': len(self.closed_trades),
'winning_trades': len([p for p in profits if p > 0]),
'losing_trades': len([p for p in profits if p < 0]),
'win_rate': (len([p for p in profits if p > 0]) / len(profits) * 100) if profits else 0,
'total_profit_loss': sum(profits),
'avg_profit_loss': np.mean(profits) if profits else 0,
'max_profit': max(profits) if profits else 0,
'max_loss': min(profits) if profits else 0
'total_trades': len(self.closed_trades), 'winning_trades': len([p for p in profits if p > 0]),
'losing_trades': len([p for p in profits if p < 0]), 'win_rate': (len([p for p in profits if p > 0]) / len(profits) * 100) if profits else 0,
'total_profit_loss': sum(profits), 'avg_profit_loss': np.mean(profits) if profits else 0,
'max_profit': max(profits) if profits else 0, 'max_loss': min(profits) if profits else 0
}
def save_trade_history(self, base_filename):
if not self.closed_trades: return
df = pd.DataFrame(self.closed_trades)
df.to_csv(f"{base_filename}.csv", index=False, encoding='utf-8-sig')
with open(f"{base_filename}.json", 'w', encoding='utf-8') as f:
json.dump(self.closed_trades, f, ensure_ascii=False, indent=2, default=str)
logger.info(f"交易记录已保存到 {base_filename}.csv/.json")
pd.DataFrame(self.closed_trades).to_csv(f"{base_filename}.csv", index=False, encoding='utf-8-sig')
def _load_peak_data(self):
"""从文件加载峰值数据"""
try:
if os.path.exists(self.peak_data_file):
with open(self.peak_data_file, 'r', encoding='utf-8') as f:
peak_data = json.load(f)
# logger.info(f"从文件加载峰值数据: {peak_data}")
return peak_data
else:
logger.info("峰值数据文件不存在,使用空数据")
if self.use_memory_storage:
return self.peak_data
else:
try:
if os.path.exists(self.peak_data_file):
with open(self.peak_data_file, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
except Exception as e:
logger.error(f"从文件加载峰值数据失败: {e}")
return {}
except Exception as e:
logger.error(f"加载峰值数据失败: {e}")
return {}
def _save_peak_data(self):
"""保存峰值数据到文件"""
try:
peak_data = {pos['ticket']: pos.get('peak_profit_pct', 0.0) for pos in self.positions}
with open(self.peak_data_file, 'w', encoding='utf-8') as f:
json.dump(peak_data, f, ensure_ascii=False, indent=2)
#logger.info(f"峰值数据已保存到文件: {peak_data}")
except Exception as e:
logger.error(f"保存峰值数据失败: {e}")
if self.use_memory_storage:
try:
self.peak_data.update({str(pos['ticket']): pos.get('peak_profit_pct', 0.0) for pos in self.positions})
except Exception as e:
logger.error(f"更新内存峰值数据失败: {e}")
else:
try:
peak_data = {str(pos['ticket']): pos.get('peak_profit_pct', 0.0) for pos in self.positions}
with open(self.peak_data_file, 'w', encoding='utf-8') as f:
json.dump(peak_data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"保存峰值数据到文件失败: {e}")
def cleanup_peak_data(self):
"""清理已平仓持仓的峰值数据"""
try:
if os.path.exists(self.peak_data_file):
with open(self.peak_data_file, 'r', encoding='utf-8') as f:
peak_data = json.load(f)
# 获取当前持仓的ticket列表
current_tickets = {pos['ticket'] for pos in self.positions}
# 清理已平仓持仓的数据
cleaned_peak_data = {ticket: peak for ticket, peak in peak_data.items()
if ticket in current_tickets}
# 保存清理后的数据
with open(self.peak_data_file, 'w', encoding='utf-8') as f:
json.dump(cleaned_peak_data, f, ensure_ascii=False, indent=2)
logger.info(f"清理峰值数据完成,保留 {len(cleaned_peak_data)} 个持仓数据")
except Exception as e:
logger.error(f"清理峰值数据失败: {e}")
current_tickets = {str(pos['ticket']) for pos in self.positions}
if self.use_memory_storage:
try:
self.peak_data = {ticket: peak for ticket, peak in self.peak_data.items() if ticket in current_tickets}
except Exception as e:
logger.error(f"清理内存峰值数据失败: {e}")
else:
try:
if os.path.exists(self.peak_data_file):
with open(self.peak_data_file, 'r', encoding='utf-8') as f:
peak_data = json.load(f)
cleaned_peak_data = {ticket: peak for ticket, peak in peak_data.items() if ticket in current_tickets}
with open(self.peak_data_file, 'w', encoding='utf-8') as f:
json.dump(cleaned_peak_data, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"清理文件峰值数据失败: {e}")
+119
View File
@@ -0,0 +1,119 @@
import os
import pandas as pd
import MetaTrader5 as mt5
from datetime import datetime, timedelta
from time import sleep
# --- 配置 ---
# 要获取的交易品种
SYMBOL = "XAUUSD"
# K线周期
TIMEFRAME = mt5.TIMEFRAME_M1
# 需要获取的数据的总起始日期
TOTAL_START_DATE = "2024-01-01"
# 输出文件名
OUTPUT_FILE = "full_historical_data.parquet"
# MT5单次请求的最大K线数 (保守起见,略低于10万)
CHUNK_SIZE = 90000
# --- 主函数 ---
def download_historical_data():
"""连接MT5,分块下载历史数据,并保存到Parquet文件。"""
print("--- 开始历史数据下载任务 ---")
# 连接到MetaTrader 5
if not mt5.initialize():
print("MT5初始化失败, 请检查终端是否开启。")
mt5.shutdown()
return
print(f"已成功连接到MT5: {mt5.terminal_info()}")
# 将字符串日期转换为datetime对象
start_date_limit = datetime.strptime(TOTAL_START_DATE, '%Y-%m-%d')
# 我们从现在开始,往回获取数据
end_date_of_chunk = datetime.now()
all_rates_df = []
total_bars_fetched = 0
while end_date_of_chunk > start_date_limit:
print(f"正在获取 {end_date_of_chunk.strftime('%Y-%m-%d')} 之前的 {CHUNK_SIZE} 条数据...")
# 从指定日期开始向前获取数据
try:
rates = mt5.copy_rates_from(SYMBOL, TIMEFRAME, end_date_of_chunk, CHUNK_SIZE)
except Exception as e:
print(f"从MT5获取数据时发生错误: {e}")
rates = None
if rates is None or len(rates) == 0:
print("没有更多数据返回,或与服务器通信失败。结束下载。")
break
# 将元组列表转换为DataFrame
rates_df = pd.DataFrame(rates)
all_rates_df.append(rates_df)
# 计算下一次请求的结束日期
# 新的结束点是当前获取到的数据块中最早的时间点
earliest_time_in_chunk = rates_df['time'].iloc[0]
end_date_of_chunk = pd.to_datetime(earliest_time_in_chunk, unit='s')
total_bars_fetched += len(rates)
print(f"已获取 {len(rates)} 条数据。最早的数据点: {end_date_of_chunk.strftime('%Y-%m-%d')}, 总计已获取: {total_bars_fetched}")
# 如果获取到的最早日期已经越过了我们的总起始日期,就停止
if end_date_of_chunk <= start_date_limit:
print("已达到设定的总起始日期,下载完成。")
break
# 短暂休眠,避免过于频繁地请求服务器
sleep(0.5)
# --- 数据处理 ---
if not all_rates_df:
print("未能获取到任何数据,程序退出。")
mt5.shutdown()
return
print("\n--- 开始数据合并与清洗 ---")
# 合并所有数据块
full_df = pd.concat(all_rates_df, ignore_index=True)
print(f"合并后总行数: {len(full_df)}")
# 去除重复数据(基于时间戳)
full_df.drop_duplicates(subset='time', inplace=True)
print(f"去除重复后总行数: {len(full_df)}")
# 将时间戳转换为datetime对象,并设置为索引
full_df['time'] = pd.to_datetime(full_df['time'], unit='s')
# 按时间排序
full_df.sort_values('time', inplace=True)
print("数据已按时间排序。")
# 将time列设为索引
full_df.set_index('time', inplace=True)
# --- 保存文件 ---
try:
full_df.to_parquet(OUTPUT_FILE)
print(f"\n--- 任务成功 ---")
print(f"数据已成功保存到: {os.path.abspath(OUTPUT_FILE)}")
print(f"数据范围: 从 {full_df.index[0]}{full_df.index[-1]}")
print(f"总计K线数量: {len(full_df)}")
except Exception as e:
print(f"保存到Parquet文件失败: {e}")
# 关闭与MetaTrader 5的连接
mt5.shutdown()
if __name__ == "__main__":
download_historical_data()
+159 -20
View File
@@ -1,35 +1,171 @@
import time
import signal
import sys
import json
import pandas as pd
from datetime import datetime
from logger import logger
from config import SYMBOL, TIMEFRAME, REALTIME_CONFIG, SIGNAL_THRESHOLDS
from config import SYMBOL, TIMEFRAME, REALTIME_CONFIG
from core.risk import RiskController
from execution.dynamic_weights import DynamicWeightManager
from strategies.wave_theory import WaveTheoryStrategy
from strategies.ma_cross import MACrossStrategy
from strategies.rsi import RSIStrategy
from strategies.bollinger import BollingerStrategy
from strategies.mean_reversion import MeanReversionStrategy
from strategies.momentum_breakout import MomentumBreakoutStrategy
from strategies.macd import MACDStrategy
from strategies.kdj import KDJStrategy
from strategies.turtle import TurtleStrategy
from strategies.daily_breakout import DailyBreakoutStrategy
class RealtimeTrader:
"""实时交易器 (已重构为依赖注入)"""
"""实时交易器 (已重构为依赖注入和市场状态参数切换)"""
def __init__(self, data_provider, update_interval=60):
self.data_provider = data_provider
self.update_interval = update_interval
self.running = False
self.risk_controller = None
self.weight_manager = None
self.regime_params = {}
self.regime_detector = None
self.current_regime = 'Ranging' # Default regime
self.strategy_instances = {}
self.current_weights = {}
self.signal_thresholds = {}
self.strategy_config_key_to_class = {
"ma_cross": MACrossStrategy,
"rsi": RSIStrategy,
"bollinger": BollingerStrategy,
"mean_reversion": MeanReversionStrategy,
"momentum_breakout": MomentumBreakoutStrategy,
"macd": MACDStrategy,
"kdj": KDJStrategy,
"turtle": TurtleStrategy,
"daily_breakout": DailyBreakoutStrategy,
"wave_theory": WaveTheoryStrategy
}
def _initialize(self):
if not self.data_provider.initialize():
return False
self.risk_controller = RiskController(self.data_provider)
self.weight_manager = DynamicWeightManager(self.data_provider)
# Load regime parameters
try:
with open('regime_optimal_params.json', 'r') as f:
self.regime_params = json.load(f)
logger.info("成功加载市场状态参数文件: regime_optimal_params.json")
except FileNotFoundError:
logger.error("错误: 未找到 regime_optimal_params.json。请先运行 regime_optimizer.py。")
return False
except json.JSONDecodeError:
logger.error("错误: regime_optimal_params.json 文件格式不正确。")
return False
# Instantiate regime detector
self.regime_detector = WaveTheoryStrategy(self.data_provider, SYMBOL, TIMEFRAME)
# Create strategy instances
self._create_strategy_instances()
self.risk_controller.sync_state()
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
logger.info("实时交易系统初始化完成")
return True
def _create_strategy_instances(self):
for name, strategy_class in self.strategy_config_key_to_class.items():
# Use the name that corresponds to the keys in the parameter file (e.g., 'MACrossStrategy')
class_name = strategy_class.__name__
self.strategy_instances[class_name] = strategy_class(self.data_provider, SYMBOL, TIMEFRAME)
logger.info(f"创建了 {len(self.strategy_instances)} 个策略实例")
def _update_parameters_for_regime(self):
# 1. Determine current regime
df_history = self.data_provider.get_historical_data(SYMBOL, TIMEFRAME, count=200)
if df_history is None or df_history.empty:
logger.warning("无法获取历史数据来判断市场状态,将使用上一个状态。")
return
df_history = pd.DataFrame(df_history)
df_history.set_index(pd.to_datetime(df_history['time'], unit='s'), inplace=True)
df_with_indicators = self.regime_detector._calculate_indicators(df_history.copy())
last_row = df_with_indicators.iloc[-1]
new_regime = "Ranging" # Default
if not pd.isna(last_row['adx']):
if last_row['adx'] < self.regime_detector.adx_threshold:
new_regime = "Ranging"
elif last_row['ema_short'] > last_row['ema_medium'] > last_row['ema_long']:
new_regime = "Uptrend"
else:
new_regime = "Downtrend"
# 2. If regime changed, update parameters
if new_regime != self.current_regime:
self.current_regime = new_regime
logger.info(f"市场状态已切换为: {self.current_regime}")
regime_config = self.regime_params.get(self.current_regime)
if not regime_config:
logger.error(f"在参数文件中未找到状态 {self.current_regime} 的配置,将使用默认参数。")
return
params = regime_config.get('best_parameters', {})
# Group parameters by component (strategy, risk, etc.)
strategy_params = {strat_name: {} for strat_name in self.strategy_instances.keys()}
risk_params = {}
temp_weights = {}
prefix_map = {
'MACrossStrategy': 'ma_cross_',
'RSIStrategy': 'rsi_',
'BollingerStrategy': 'bollinger_',
'MACDStrategy': 'macd_',
'MeanReversionStrategy': 'mean_reversion_',
'MomentumBreakoutStrategy': 'momentum_breakout_',
'KDJStrategy': 'kdj_',
'TurtleStrategy': 'turtle_',
'DailyBreakoutStrategy': 'daily_breakout_',
'WaveTheoryStrategy': 'wave_'
}
for key, value in params.items():
if key.startswith('weight_'):
strategy_name = key.replace('weight_', '')
temp_weights[strategy_name] = value
elif key in ['buy_threshold', 'sell_threshold']:
self.signal_thresholds[key] = value
elif hasattr(self.risk_controller, key):
risk_params[key] = value
else:
for strat_name, prefix in prefix_map.items():
if key.startswith(prefix):
param_name = key.replace(prefix, '')
# BUG FIX: Handle the 'wave_period' special case
if strat_name == 'WaveTheoryStrategy' and key == 'wave_period':
param_name = 'wave_period'
strategy_params[strat_name][param_name] = value
break
# Update parameters in a batch
for strat_name, params_dict in strategy_params.items():
if params_dict:
self.strategy_instances[strat_name].set_params(params_dict)
if risk_params:
for key, value in risk_params.items():
setattr(self.risk_controller, key, value)
self.current_weights = temp_weights
logger.info(f"已为 {self.current_regime} 状态加载新参数和权重。")
def _signal_handler(self, signum, frame):
logger.info(f"接收到信号 {signum},准备退出...")
@@ -38,22 +174,29 @@ class RealtimeTrader:
def _run_cycle(self):
try:
self.risk_controller.sync_state()
# Update parameters based on current market regime
self._update_parameters_for_regime()
current_price = self.data_provider.get_current_price(SYMBOL)
if not current_price:
logger.warning("无法获取当前价格,跳过本次循环")
return
strategies_with_weights = self.weight_manager.get_current_strategies_and_weights()
signals, weights = [], []
strategies_with_weights = []
for strat_name, strat_instance in self.strategy_instances.items():
weight = self.current_weights.get(strat_name, 0.0)
if weight > 0: # Only calculate signal if weight is positive
strategies_with_weights.append((strat_instance, weight))
signals.append(strat_instance.generate_signal())
weights.append(weight)
if not strategies_with_weights: return
signals, weights = [], []
for strat, weight in strategies_with_weights:
signals.append(strat.generate_signal())
weights.append(weight)
# 打印详细信号日志
logger.info("--- 信号计算详情 ---")
logger.info(f"--- 信号计算详情 (状态: {self.current_regime}) ---")
for i, (strat, weight) in enumerate(strategies_with_weights):
signal = signals[i]
weighted_signal = signal * weight
@@ -63,11 +206,9 @@ class RealtimeTrader:
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 = self.signal_thresholds.get('buy_threshold', 1.5)
sell_threshold = self.signal_thresholds.get('sell_threshold', -1.5)
logger.info(f"加权信号: {weighted_signal_sum:.2f} (买入阈值: {buy_threshold}, 卖出阈值: {sell_threshold})")
# logger.info(f"信号比较: {weighted_signal_sum} > {buy_threshold} = {weighted_signal_sum > buy_threshold}")
# logger.info(f"信号比较: {weighted_signal_sum} < {sell_threshold} = {weighted_signal_sum < sell_threshold}")
direction = None
if weighted_signal_sum > buy_threshold:
@@ -94,8 +235,7 @@ class RealtimeTrader:
logger.info(f" 当前持仓: {len(open_positions)}")
for pos in open_positions:
pnl_pct = self.risk_controller.position_manager._calculate_pnl_pct(pos, current_price['last'])
# 计算持仓时间
holding_time = current_price['time'] - pos['entry_time']
holding_time = datetime.fromtimestamp(current_price['time']) - pos['entry_time']
holding_minutes = holding_time.total_seconds() / 60
logger.info(f" - Ticket {pos['ticket']}: {pos['position_type']} {pos['symbol']} @ {pos['entry_price']:.2f} | 持仓时间: {holding_minutes:.1f}分钟 | 浮动盈亏: {pnl_pct:.2%}")
@@ -137,5 +277,4 @@ class RealtimeTrader:
finally:
self.data_provider.shutdown()
logger.info("实时交易系统已停止")
sys.exit(0)
sys.exit(0)
Binary file not shown.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
按“机器学习/模型预测优化”思路来改,结构可以这样调整:
---
## 改法核心
1. 保留 **回测调用逻辑**(原来 GA 的 `evaluate()` 或 `fitness()` 部分),因为它是核心的盈利数计算。
2. 去掉 GA 的选择/交叉/变异部分,改成:
* **第一阶段(探索)**:随机生成 N 组参数 → 跑回测 → 存成 `(params, profit)`
* **第二阶段(利用)**:训练一个预测模型(例如 `RandomForestRegressor`)来拟合 `(params → profit)`
* **第三阶段(优化)**:用模型快速评估大量随机参数 → 挑预测盈利最高的 Top N → 实际回测确认
---
在这个 `optimizer.py` 里:
* 保留现有的 **参数定义、回测调用、指标计算**
* 替换 GA 循环为 **“随机探索 + 模型预测筛选”**
* 把结果输出成和现在 GA 一样的格式(方便和旧版本对比)
这样就能直接运行,不需要重写项目结构。
保留现有所有参数配置和回测入口,只替换优化器核心逻辑。
+28
View File
@@ -0,0 +1,28 @@
"""
MT5常量定义文件
避免在config.py中直接导入MetaTrader5
"""
# MT5时间周期常量
TIMEFRAME_M1 = 1 # 1分钟
TIMEFRAME_M5 = 5 # 5分钟
TIMEFRAME_M15 = 15 # 15分钟
TIMEFRAME_M30 = 30 # 30分钟
TIMEFRAME_H1 = 16385 # 1小时
TIMEFRAME_H4 = 16388 # 4小时
TIMEFRAME_D1 = 16408 # 1天
TIMEFRAME_W1 = 32769 # 1周
TIMEFRAME_MN1 = 49153 # 1月
# 常用时间周期映射
TIMEFRAME_MAP = {
'M1': TIMEFRAME_M1,
'M5': TIMEFRAME_M5,
'M15': TIMEFRAME_M15,
'M30': TIMEFRAME_M30,
'H1': TIMEFRAME_H1,
'H4': TIMEFRAME_H4,
'D1': TIMEFRAME_D1,
'W1': TIMEFRAME_W1,
'MN1': TIMEFRAME_MN1,
}
+1 -1
View File
@@ -287,7 +287,7 @@ def evaluate_fitness(individual, df_data):
# 获取风险管理参数
risk_params = {
'stop_loss_pct': parsed_params.get('stop_loss_pct', RISK_CONFIG.get('stop_loss_pct', -0.01)),
'profit_retracement_pct': parsed_params.get('profit_retracement_pct', RISK_CONFIG.get('profit_retracement_pct', 0.10)),
'profit_retracement_pct': parsed_params.get('profit_retracement_pct', .get('profit_retracement_pct', 0.10)),
'min_profit_for_trailing': parsed_params.get('min_profit_for_trailing', RISK_CONFIG.get('min_profit_for_trailing', 0.01)),
'take_profit_pct': parsed_params.get('take_profit_pct', RISK_CONFIG.get('take_profit_pct', 0.20)),
'max_holding_minutes': parsed_params.get('max_holding_minutes', RISK_CONFIG.get('max_holding_minutes', 60)),
+510
View File
@@ -0,0 +1,510 @@
import json
import csv
from datetime import datetime
import os
import sys
import numpy as np
import random
from tqdm import tqdm
import multiprocessing
import pandas as pd
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
import joblib
# 固定随机种子,确保可复现
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Core components for backtesting
from core.data_providers import BacktestDataProvider
from core.risk import RiskController
from execution.dynamic_weights import DynamicWeightManager
from config import (
SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE,
USE_DATE_RANGE, BACKTEST_CONFIG, INITIAL_CAPITAL, SIGNAL_THRESHOLDS,
RISK_CONFIG, GENETIC_OPTIMIZER_CONFIG
)
from core.utils import get_rates, initialize, shutdown
# Import all strategy classes
from strategies.ma_cross import MACrossStrategy
from strategies.rsi import RSIStrategy
from strategies.bollinger import BollingerStrategy
from strategies.mean_reversion import MeanReversionStrategy
from strategies.momentum_breakout import MomentumBreakoutStrategy
from strategies.macd import MACDStrategy
from strategies.kdj import KDJStrategy
from strategies.turtle import TurtleStrategy
from strategies.daily_breakout import DailyBreakoutStrategy
from strategies.wave_theory import WaveTheoryStrategy
# --- Global Data (Loaded once) ---
df_historical_data = None
# 模块级别的初始化函数,用于多进程
def init_worker():
import logging
logging.getLogger().setLevel(logging.WARNING)
logging.getLogger("StrategyLogger").setLevel(logging.WARNING)
logging.getLogger("PositionManager").setLevel(logging.WARNING)
logging.getLogger("RiskController").setLevel(logging.WARNING)
logging.getLogger("DataProvider").setLevel(logging.WARNING)
def load_historical_data():
global df_historical_data
if df_historical_data is not None:
return df_historical_data
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()
if rates is None:
print("获取历史数据失败,退出。")
sys.exit(1)
df = pd.DataFrame(rates)
df.set_index(pd.to_datetime(df['time'], unit='s'), inplace=True)
if len(df) > OPTIMIZER_COUNT:
df = df.iloc[-OPTIMIZER_COUNT:]
df_historical_data = df
return df
# --- Parameter Definition ---
# (保持与原 optimizer.py 一致)
PARAMETER_DEFINITIONS = [
# MACrossStrategy parameters
{'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 parameters
{'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 parameters
{'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 parameters
{'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 parameters
{'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 parameters
{'name': 'momentum_breakout_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MomentumBreakoutStrategy'},
# KDJStrategy parameters
{'name': 'kdj_period', 'type': 'int', 'min': 5, 'max': 21, 'strategy': 'KDJStrategy'},
# TurtleStrategy parameters
{'name': 'turtle_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'TurtleStrategy'},
# WaveTheoryStrategy parameters
{'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 parameters
{'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 parameters
{'name': 'stop_loss_pct', 'type': 'float', 'min': -0.02, 'max': -0.0005, '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.01, 'strategy': 'risk'},
{'name': 'take_profit_pct', 'type': 'float', 'min': 0.10, 'max': 1.0, '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 (for all strategies)
{'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'},
]
# Map strategy class names to config keys for weights (for logging/display)
strategy_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"
}
# Map config keys to strategy class names (for instantiation)
strategy_config_key_to_class = {
"ma_cross": MACrossStrategy,
"rsi": RSIStrategy,
"bollinger": BollingerStrategy,
"mean_reversion": MeanReversionStrategy,
"momentum_breakout": MomentumBreakoutStrategy,
"macd": MACDStrategy,
"kdj": KDJStrategy,
"turtle": TurtleStrategy,
"daily_breakout": DailyBreakoutStrategy,
"wave_theory": WaveTheoryStrategy
}
# --- Backtest Evaluation Function ---
def evaluate_fitness(params_tuple, df_data):
"""
执行回测并计算夏普比率作为评估指标
(已移除DynamicWeightManager使用固定的优化权重)
"""
# --- 1. 参数解析 ---
individual = list(params_tuple)
parsed_params = {param_def['name']: (int(round(val)) if param_def['type'] == 'int' else val)
for param_def, val in zip(PARAMETER_DEFINITIONS, individual)}
strategy_params_dict = {}
for param_def in PARAMETER_DEFINITIONS:
strategy_name = param_def.get('strategy')
if strategy_name and strategy_name not in ['weight', 'signal', 'risk', 'market_state', 'trend_weights', 'trend_thresholds', 'confidence']:
if strategy_name not in strategy_params_dict:
strategy_params_dict[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 == '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_dict[strategy_name][param_name] = parsed_params[param_def['name']]
strategies_for_backtest_instance = {
config_key: strategy_class(None, SYMBOL, TIMEFRAME, **strategy_params_dict.get(strategy_class.__name__, {}))
for config_key, strategy_class in strategy_config_key_to_class.items()
}
suggested_weights = {p['name'].replace('weight_', ''): parsed_params[p['name']] for p in PARAMETER_DEFINITIONS if p.get('strategy') == 'weight'}
signal_thresholds = {'buy_threshold': parsed_params.get('buy_threshold', 1.0), 'sell_threshold': parsed_params.get('sell_threshold', -1.0)}
risk_params = {p['name']: parsed_params[p['name']] for p in PARAMETER_DEFINITIONS if p.get('strategy') == 'risk'}
# --- 2. 回测设置 ---
df = df_data.copy()
data_provider = BacktestDataProvider(df, initial_equity=INITIAL_CAPITAL)
risk_controller = RiskController(data_provider, trade_direction=BACKTEST_CONFIG['trade_direction'])
for key, value in risk_params.items():
if hasattr(risk_controller, key): setattr(risk_controller, key, value)
# --- 3. 信号生成 ---
all_signals_df = pd.DataFrame(index=df.index)
for config_key, strat_instance in strategies_for_backtest_instance.items():
if hasattr(strat_instance, 'run_backtest'):
all_signals_df[strat_instance.name] = strat_instance.run_backtest(df.copy())
if all_signals_df.empty: return (0.0,)
# --- 4. 信号加权 (使用优化器传入的固定权重) ---
weights_arr = []
for name, sig in all_signals_df.items():
weight_key = name.replace('Strategy', '')
w = suggested_weights.get(weight_key, 1.0)
weights_arr.append(sig.values * w)
combined = np.sum(np.column_stack(weights_arr), axis=1)
final_signals = np.where(combined > signal_thresholds['buy_threshold'], 1, np.where(combined < signal_thresholds['sell_threshold'], -1, 0))
# --- 5. 回测循环与权益曲线记录 ---
equity_curve = []
equity_dates = []
symbol_info = data_provider.get_symbol_info(SYMBOL)
contract_size = symbol_info['trade_contract_size'] if symbol_info else 100
for i in range(len(df)):
current_price = data_provider.get_current_price(SYMBOL)
if not current_price: continue
direction = "buy" if final_signals[i] == 1 else "sell" if final_signals[i] == -1 else None
if direction:
risk_controller.process_trading_signal(direction, current_price, abs(final_signals[i]))
risk_controller.monitor_positions(current_price)
realized_pnl = sum(t['profit_loss'] for t in risk_controller.position_manager.closed_trades)
unrealized_pnl = sum(
risk_controller.position_manager._calculate_pnl_pct(pos, current_price['last']) * pos['entry_price'] * pos['quantity'] * contract_size
for pos in risk_controller.position_manager.positions
)
current_equity = INITIAL_CAPITAL + realized_pnl + unrealized_pnl
equity_curve.append(current_equity)
equity_dates.append(df.index[i])
data_provider.tick()
# --- 6. 夏普比率计算 ---
if not equity_curve: return (0.0,)
equity_series = pd.Series(equity_curve, index=pd.to_datetime(equity_dates))
daily_equity = equity_series.resample('D').last().ffill()
daily_returns = daily_equity.pct_change().dropna()
if daily_returns.empty or daily_returns.std() == 0: return (0.0,)
sharpe_ratio = (daily_returns.mean() / daily_returns.std()) * np.sqrt(252)
return (sharpe_ratio if np.isfinite(sharpe_ratio) else 0.0,)
# --- Helper Functions ---
def generate_random_params():
"""生成一组随机参数"""
params = []
for param_def in PARAMETER_DEFINITIONS:
if param_def['type'] == 'int':
params.append(random.randint(param_def['min'], param_def['max']))
else:
params.append(random.uniform(param_def['min'], param_def['max']))
return tuple(params)
def run_backtest_for_params(args):
"""用于多进程回测的包装函数, 现在返回夏普比率"""
params, df_data = args
try:
sharpe = evaluate_fitness(params, df_data)[0]
return params, sharpe
except Exception as e:
return params, e # 返回异常对象本身
# --- Main Optimizer Logic ---
def run_optimizer(df_to_optimize=None):
"""
主优化器函数采用探索-利用策略, 基于夏普比率
"""
try:
if df_to_optimize is not None:
# print("优化器正在使用传入的数据...")
df_historical_data = df_to_optimize
else:
# print("优化器正在加载默认历史数据...")
df_historical_data = load_historical_data()
ML_OPTIMIZER_CONFIG = {
"exploration_samples": 500,
"prediction_samples": 10000,
"top_n_candidates": 10,
"enable_multiprocessing": True,
"processes": multiprocessing.cpu_count() - 1 or 1,
"model_filename": "sharpe_predictor_model.joblib"
}
# --- 1. 探索阶段 ---
print(f"--- 阶段 1: 探索 (生成 {ML_OPTIMIZER_CONFIG['exploration_samples']} 组随机参数以计算夏普比率) ---")
exploration_params = [generate_random_params() for _ in range(ML_OPTIMIZER_CONFIG['exploration_samples'])]
exploration_results = []
if ML_OPTIMIZER_CONFIG["enable_multiprocessing"]:
pool = multiprocessing.Pool(processes=ML_OPTIMIZER_CONFIG["processes"], initializer=init_worker)
tasks = [(p, df_historical_data) for p in exploration_params]
with tqdm(total=len(tasks), desc="探索回测中 (计算夏普比率)") as pbar:
for result in pool.imap_unordered(run_backtest_for_params, tasks):
exploration_results.append(result)
pbar.update()
pool.close()
pool.join()
else:
for params in tqdm(exploration_params, desc="探索回测中 (计算夏普比率)"):
exploration_results.append(run_backtest_for_params((params, df_historical_data)))
exploration_df = pd.DataFrame(exploration_results, columns=['params', 'sharpe'])
# 筛选出失败和成功的结果
failed_mask = exploration_df['sharpe'].apply(lambda x: isinstance(x, Exception))
failed_results = exploration_df[failed_mask]
if not failed_results.empty:
print(f"\n--- 错误分析: 发现 {len(failed_results)}/{len(exploration_df)} 个失败的回测 ---")
# 统计独特的错误信息
error_counts = failed_results['sharpe'].astype(str).value_counts()
print("错误类型统计:")
for error, count in error_counts.items():
print(f" - [{count}次] {error}")
print("--------------------------------------------------\n")
exploration_df = exploration_df[~failed_mask] # 使用反向掩码筛选有效结果
print(f"探索完成,获得 {len(exploration_df)} 个有效回测结果。")
# --- 2. 利用阶段 (模型训练) ---
print("\n--- 阶段 2: 利用 (训练夏普比率预测模型) ---")
if len(exploration_df) < 2:
print("有效数据不足,无法训练模型。")
return
X = np.array(list(exploration_df['params']))
y = exploration_df['sharpe'].values
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=SEED)
model = RandomForestRegressor(n_estimators=100, random_state=SEED, n_jobs=-1)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
mse = mean_squared_error(y_test, y_pred)
print(f"模型训练完成。测试集均方误差 (MSE): {mse:.4f}")
joblib.dump(model, ML_OPTIMIZER_CONFIG['model_filename'])
print(f"模型已保存到: {ML_OPTIMIZER_CONFIG['model_filename']}")
# --- 3. 优化阶段 (模型预测与验证) ---
print(f"\n--- 阶段 3: 优化 (使用模型预测 {ML_OPTIMIZER_CONFIG['prediction_samples']} 组参数的夏普比率) ---")
prediction_params = [generate_random_params() for _ in range(ML_OPTIMIZER_CONFIG['prediction_samples'])]
predicted_sharpes = model.predict(np.array(prediction_params))
top_n_indices = np.argsort(predicted_sharpes)[-ML_OPTIMIZER_CONFIG['top_n_candidates']:]
top_n_candidates = [tuple(p) for p in np.array(prediction_params)[top_n_indices]]
print(f"模型预测完成,选出 Top {ML_OPTIMIZER_CONFIG['top_n_candidates']} 组候选参数进行最终验证。")
# --- 4. 最终验证 ---
print("\n--- 阶段 4: 最终验证 (回测 Top N 候选参数) ---")
final_results = []
if ML_OPTIMIZER_CONFIG["enable_multiprocessing"]:
pool = multiprocessing.Pool(processes=ML_OPTIMIZER_CONFIG["processes"], initializer=init_worker)
tasks = [(p, df_historical_data) for p in top_n_candidates]
with tqdm(total=len(tasks), desc="最终验证回测中") as pbar:
for result in pool.imap_unordered(run_backtest_for_params, tasks):
final_results.append(result)
pbar.update()
pool.close()
pool.join()
else:
for params in tqdm(top_n_candidates, desc="最终验证回测中"):
final_results.append(run_backtest_for_params((params, df_historical_data)))
best_result = max(final_results, key=lambda item: item[1] if isinstance(item[1], (int, float)) else -float('inf'))
best_params_tuple, best_sharpe = best_result
best_params_dict = {param_def['name']: (int(round(val)) if param_def['type'] == 'int' else val) for idx, (param_def, val) in enumerate(zip(PARAMETER_DEFINITIONS, best_params_tuple))}
print("\n--- 优化结束 ---")
print(f"最佳夏普比率: {best_sharpe:.4f}")
print("最佳参数组合:")
for key, value in best_params_dict.items():
print(f" {key}: {value}")
save_optimization_results(best_params_dict, best_sharpe, exploration_df, final_results)
return best_params_dict, best_sharpe
except Exception as e:
print(f"优化器运行出错: {e}")
import traceback
print(traceback.format_exc())
sys.exit(1)
def save_optimization_results(best_params, best_fitness, exploration_df, final_results_list):
"""保存优化结果到文件"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
final_results_df = pd.DataFrame(final_results_list, columns=['params', 'sharpe'])
results = {
'optimization_info': {
'timestamp': timestamp,
'best_fitness_metric': 'Sharpe Ratio',
'best_fitness_value': float(best_fitness),
'optimizer_type': 'Machine Learning Based',
'random_seed': SEED
},
'best_parameters': best_params,
'exploration_summary': {
'count': len(exploration_df),
'max_sharpe': exploration_df['sharpe'].max(),
'avg_sharpe': exploration_df['sharpe'].mean()
},
'final_candidates': final_results_df.to_dict('records')
}
json_filename = f"ml_optimization_results_{timestamp}.json"
with open(json_filename, 'w', encoding='utf-8') as f:
class NpEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (np.integer, np.floating)): return float(obj)
if isinstance(obj, np.ndarray): return obj.tolist()
return super(NpEncoder, self).default(obj)
json.dump(results, f, ensure_ascii=False, indent=2, cls=NpEncoder)
print(f"\n优化结果已保存到: {json_filename}")
csv_filename = f"ml_optimization_best_params_{timestamp}.csv"
with open(csv_filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Parameter', 'Value'])
for key, value in best_params.items():
writer.writerow([key, value])
print(f"最佳参数已保存到: {csv_filename}")
except Exception as e:
print(f"保存优化结果时出错: {e}")
import traceback
print(traceback.format_exc())
if __name__ == "__main__":
run_optimizer()
+686
View File
@@ -0,0 +1,686 @@
"""
机器学习优化器 (基于随机探索 + 模型预测)
替代遗传算法使用三阶段优化策略
1. 随机探索阶段
2. 模型训练阶段
3. 预测筛选阶段
"""
import random
import numpy as np
import pandas as pd
import sys
import os
from datetime import datetime
from tqdm import tqdm
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
import json
import csv
import multiprocessing
from joblib import Parallel, delayed
# 固定随机种子,确保可复现
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Core components for backtesting
from core.data_providers import BacktestDataProvider
from core.risk import RiskController
from execution.dynamic_weights import DynamicWeightManager
from config import (
SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE,
USE_DATE_RANGE, BACKTEST_CONFIG, INITIAL_CAPITAL, SIGNAL_THRESHOLDS, DEFAULT_WEIGHTS,
RISK_CONFIG, GENETIC_OPTIMIZER_CONFIG, MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS,
TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS
)
from core.utils import get_rates, initialize, shutdown
# Import all strategy classes
from strategies.ma_cross import MACrossStrategy
from strategies.rsi import RSIStrategy
from strategies.bollinger import BollingerStrategy
from strategies.mean_reversion import MeanReversionStrategy
from strategies.momentum_breakout import MomentumBreakoutStrategy
from strategies.macd import MACDStrategy
from strategies.kdj import KDJStrategy
from strategies.turtle import TurtleStrategy
from strategies.daily_breakout import DailyBreakoutStrategy
from strategies.wave_theory import WaveTheoryStrategy
# --- Global Data (Loaded once) ---
df_historical_data = None
# --- Parameter Definition (Same as GA optimizer) ---
# Define the search space for strategy parameters and weights
PARAMETER_DEFINITIONS = [
# MACrossStrategy parameters
{'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 parameters
{'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 parameters
{'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 parameters
{'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 parameters
{'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 parameters
{'name': 'momentum_breakout_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MomentumBreakoutStrategy'},
# KDJStrategy parameters
{'name': 'kdj_period', 'type': 'int', 'min': 5, 'max': 21, 'strategy': 'KDJStrategy'},
# TurtleStrategy parameters
{'name': 'turtle_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'TurtleStrategy'},
# WaveTheoryStrategy parameters
{'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 parameters
{'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 parameters
{'name': 'stop_loss_pct', 'type': 'float', 'min': -0.02, 'max': -0.005, '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 (for all strategies)
{'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'},
]
# Map config keys to strategy class names (for instantiation)
strategy_config_key_to_class = {
"ma_cross": MACrossStrategy,
"rsi": RSIStrategy,
"bollinger": BollingerStrategy,
"mean_reversion": MeanReversionStrategy,
"momentum_breakout": MomentumBreakoutStrategy,
"macd": MACDStrategy,
"kdj": KDJStrategy,
"turtle": TurtleStrategy,
"daily_breakout": DailyBreakoutStrategy,
"wave_theory": WaveTheoryStrategy
}
def load_historical_data():
"""加载历史数据(与原优化器相同)"""
global df_historical_data
if df_historical_data is not None:
return df_historical_data
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()
if rates is None:
print("获取历史数据失败,退出。")
sys.exit(1)
df = pd.DataFrame(rates)
df.set_index(pd.to_datetime(df['time'], unit='s'), inplace=True)
if len(df) > OPTIMIZER_COUNT:
df = df.iloc[-OPTIMIZER_COUNT:]
df_historical_data = df
return df_historical_data
def generate_random_parameters():
"""生成随机参数组合"""
params = []
for param_def in PARAMETER_DEFINITIONS:
if param_def['type'] == 'int':
value = random.randint(param_def['min'], param_def['max'])
else: # float
value = random.uniform(param_def['min'], param_def['max'])
params.append(value)
return params
def evaluate_parameters(params_array, df_data):
"""评估参数组合的适应度(与原优化器相同)"""
parsed_params = {}
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
val = params_array[idx]
parsed_params[param_def['name']] = int(round(val)) if param_def['type'] == 'int' else val
# 构建策略参数字典(与原优化器相同)
strategy_params_dict = {}
for param_def in PARAMETER_DEFINITIONS:
strategy_name = param_def.get('strategy')
if strategy_name and strategy_name not in ['weight', 'signal', 'risk', 'market_state', 'trend_weights', 'trend_thresholds', 'confidence']:
if strategy_name not in strategy_params_dict:
strategy_params_dict[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.endswith('_period') and 'ma_cross' not in param_name:
param_name = 'period'
elif param_name == 'bollinger_std_dev':
param_name = 'std_dev'
elif param_name == 'mean_reversion_std_dev':
param_name = 'std_dev'
elif param_name.startswith('macd_'):
param_name = param_name[5:] # 移除 "macd_" 前缀
elif param_name.startswith('wave_'):
param_name = param_name[5:] # 移除 "wave_" 前缀
if param_name == "period":
param_name = "wave_period"
elif param_name == 'daily_breakout_bars_count':
param_name = 'bars_count'
strategy_params_dict[strategy_name][param_name] = parsed_params[param_def['name']]
# 策略实例化
strategies_for_backtest_instance = {}
for config_key, strategy_class in strategy_config_key_to_class.items():
strategy_class_name = strategy_class.__name__
strat_params = strategy_params_dict.get(strategy_class_name, {})
strategies_for_backtest_instance[config_key] = strategy_class(None, SYMBOL, TIMEFRAME, **strat_params)
# 获取策略权重
suggested_weights = {
p['name'].replace('weight_', ''): parsed_params[p['name']]
for p in PARAMETER_DEFINITIONS if p.get('strategy') == 'weight'
}
# 获取信号阈值
signal_thresholds = {
'buy_threshold': parsed_params.get('buy_threshold', SIGNAL_THRESHOLDS.get('buy_threshold', 1.0)),
'sell_threshold': parsed_params.get('sell_threshold', SIGNAL_THRESHOLDS.get('sell_threshold', -1.0))
}
# 获取风险管理参数
risk_params = {
'stop_loss_pct': parsed_params.get('stop_loss_pct', RISK_CONFIG.get('stop_loss_pct', -0.01)),
'profit_retracement_pct': parsed_params.get('profit_retracement_pct', RISK_CONFIG.get('profit_retracement_pct', 0.10)),
'min_profit_for_trailing': parsed_params.get('min_profit_for_trailing', RISK_CONFIG.get('min_profit_for_trailing', 0.01)),
'take_profit_pct': parsed_params.get('take_profit_pct', RISK_CONFIG.get('take_profit_pct', 0.20)),
'max_holding_minutes': parsed_params.get('max_holding_minutes', RISK_CONFIG.get('max_holding_minutes', 60)),
'min_profit_for_time_exit': parsed_params.get('min_profit_for_time_exit', RISK_CONFIG.get('min_profit_for_time_exit', 0.005))
}
# 获取市场状态分析参数
market_state_params = {
'trend_period': parsed_params.get('market_trend_period', MARKET_STATE_CONFIG.get('trend_period', 50)),
'retracement_tolerance': parsed_params.get('market_retracement_tolerance', MARKET_STATE_CONFIG.get('retracement_tolerance', 0.30)),
'volume_period': parsed_params.get('market_volume_period', MARKET_STATE_CONFIG.get('volume_period', 20)),
'volume_ma_period': parsed_params.get('market_volume_ma_period', MARKET_STATE_CONFIG.get('volume_ma_period', 10))
}
# 获取趋势指标权重
trend_weights = {
'price_breakout': parsed_params.get('trend_price_breakout_weight', TREND_INDICATOR_WEIGHTS.get('price_breakout', 0.35)),
'volume_confirmation': parsed_params.get('trend_volume_confirmation_weight', TREND_INDICATOR_WEIGHTS.get('volume_confirmation', 0.25)),
'momentum oscillator': parsed_params.get('trend_momentum_oscillator_weight', TREND_INDICATOR_WEIGHTS.get('momentum oscillator', 0.20)),
'moving_average': parsed_params.get('trend_moving_average_weight', TREND_INDICATOR_WEIGHTS.get('moving_average', 0.20))
}
# 获取趋势阈值
trend_thresholds = {
'strong_trend': parsed_params.get('trend_strong_threshold', TREND_THRESHOLDS.get('strong_trend', 0.6)),
'weak_trend': parsed_params.get('trend_weak_threshold', TREND_THRESHOLDS.get('weak_trend', 0.3)),
'volume_spike': parsed_params.get('trend_volume_spike', TREND_THRESHOLDS.get('volume_spike', 1.5)),
'oversold': parsed_params.get('trend_oversold', TREND_THRESHOLDS.get('oversold', 30)),
'overbought': parsed_params.get('trend_overbought', TREND_THRESHOLDS.get('overbought', 70))
}
# 获取置信度阈值
confidence_thresholds = {
'high_confidence': parsed_params.get('confidence_high', CONFIDENCE_THRESHOLDS.get('high_confidence', 0.7)),
'medium_confidence': parsed_params.get('confidence_medium', CONFIDENCE_THRESHOLDS.get('medium_confidence', 0.4))
}
# 回测执行(与原优化器相同)
df = df_data.copy()
data_provider = BacktestDataProvider(df, initial_equity=INITIAL_CAPITAL)
risk_controller = RiskController(data_provider, trade_direction=BACKTEST_CONFIG['trade_direction'])
for key, value in risk_params.items():
if hasattr(risk_controller, key):
setattr(risk_controller, key, value)
dynamic_weight_manager = DynamicWeightManager(
data_provider,
market_state_params=market_state_params,
trend_weights=trend_weights,
trend_thresholds=trend_thresholds,
confidence_thresholds=confidence_thresholds
)
all_signals_df = pd.DataFrame(index=df.index)
for config_key, strat_instance in strategies_for_backtest_instance.items():
if hasattr(strat_instance, 'run_backtest') and callable(getattr(strat_instance, 'run_backtest')):
all_signals_df[strat_instance.name] = strat_instance.run_backtest(df.copy())
if all_signals_df.empty:
return 0.0
strategies_with_weights = dynamic_weight_manager.get_current_strategies_and_weights()
weights_arr = []
for name, sig in all_signals_df.items():
w = 1.0
for strat_instance, weight in strategies_with_weights:
if strat_instance.name == name:
w = weight
break
if w == 1.0:
w = suggested_weights.get(name, 1.0)
weights_arr.append(sig.values * w)
combined = np.sum(np.column_stack(weights_arr), axis=1)
buy_th, sell_th = signal_thresholds['buy_threshold'], signal_thresholds['sell_threshold']
final_signals = np.where(combined > buy_th, 1, np.where(combined < sell_th, -1, 0))
data_provider.current_index = 0
for i in range(len(df)):
current_price = data_provider.get_current_price(SYMBOL)
if not current_price:
continue
current_signal = final_signals[i]
direction = None
if current_signal == 1:
direction = "buy"
elif current_signal == -1:
direction = "sell"
if direction:
risk_controller.process_trading_signal(direction, current_price, abs(current_signal))
risk_controller.monitor_positions(current_price)
data_provider.tick()
total_profit_loss = risk_controller.position_manager.get_trade_summary().get('total_profit_loss', 0)
return total_profit_loss
def phase1_exploration(df_data, exploration_size=100):
"""第一阶段:随机探索"""
print(f"=== 第一阶段:随机探索 ===")
print(f"生成 {exploration_size} 组随机参数进行探索...")
exploration_data = []
# 使用多进程并行评估
num_cores = multiprocessing.cpu_count()
print(f"使用 {num_cores} 个核心进行并行评估...")
# 生成所有随机参数
all_params = [generate_random_parameters() for _ in range(exploration_size)]
# 并行评估
results = Parallel(n_jobs=num_cores)(
delayed(evaluate_parameters)(params, df_data)
for params in tqdm(all_params, desc="随机探索评估")
)
# 收集结果
for params, profit in zip(all_params, results):
exploration_data.append({
'params': params.copy(),
'profit': profit
})
# 按盈利排序
exploration_data.sort(key=lambda x: x['profit'], reverse=True)
print(f"随机探索完成,最佳盈利: ${exploration_data[0]['profit']:.2f}")
print(f"最差盈利: ${exploration_data[-1]['profit']:.2f}")
print(f"平均盈利: ${np.mean([d['profit'] for d in exploration_data]):.2f}")
return exploration_data
def phase2_model_training(exploration_data):
"""第二阶段:模型训练"""
print(f"\n=== 第二阶段:模型训练 ===")
# 准备训练数据
X = np.array([d['params'] for d in exploration_data])
y = np.array([d['profit'] for d in exploration_data])
# 分割训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=SEED)
print(f"训练数据: {len(X_train)} 组, 测试数据: {len(X_test)}")
# 训练随机森林模型
model = RandomForestRegressor(
n_estimators=100,
max_depth=10,
min_samples_split=5,
min_samples_leaf=2,
random_state=SEED,
n_jobs=-1
)
model.fit(X_train, y_train)
# 评估模型性能
y_pred_train = model.predict(X_train)
y_pred_test = model.predict(X_test)
train_r2 = r2_score(y_train, y_pred_train)
test_r2 = r2_score(y_test, y_pred_test)
train_mse = mean_squared_error(y_train, y_pred_train)
test_mse = mean_squared_error(y_test, y_pred_test)
print(f"模型训练完成:")
print(f"训练集 R²: {train_r2:.4f}, MSE: {train_mse:.4f}")
print(f"测试集 R²: {test_r2:.4f}, MSE: {test_mse:.4f}")
# 特征重要性分析
feature_importance = model.feature_importances_
top_features = np.argsort(feature_importance)[-10:][::-1] # 最重要的10个特征
print(f"\n最重要的10个参数:")
for idx in top_features:
param_def = PARAMETER_DEFINITIONS[idx]
print(f" {param_def['name']}: {feature_importance[idx]:.4f}")
return model
def phase3_prediction_and_validation(model, df_data, exploration_data, prediction_size=1000, top_n=20):
"""第三阶段:预测筛选和验证"""
print(f"\n=== 第三阶段:预测筛选 ===")
print(f"生成 {prediction_size} 组参数进行预测,筛选前 {top_n} 组进行验证...")
# 生成大量随机参数进行预测
prediction_params = [generate_random_parameters() for _ in range(prediction_size)]
# 使用模型预测盈利
predicted_profits = model.predict(prediction_params)
# 按预测盈利排序,选择前top_n组
top_indices = np.argsort(predicted_profits)[-top_n:][::-1]
print(f"预测筛选完成,预测最高盈利: ${predicted_profits[top_indices[0]]:.2f}")
# 对筛选出的参数进行实际验证
validation_results = []
print(f"开始验证前 {top_n} 组参数...")
for i, idx in enumerate(tqdm(top_indices, desc="验证参数")):
params = prediction_params[idx]
actual_profit = evaluate_parameters(params, df_data)
validation_results.append({
'params': params.copy(),
'predicted_profit': predicted_profits[idx],
'actual_profit': actual_profit,
'prediction_error': abs(predicted_profits[idx] - actual_profit)
})
if (i + 1) % 5 == 0:
print(f"已验证 {i + 1}/{top_n} 组,当前最佳实际盈利: ${max([r['actual_profit'] for r in validation_results[:i+1]]):.2f}")
# 按实际盈利排序
validation_results.sort(key=lambda x: x['actual_profit'], reverse=True)
best_validation = validation_results[0]
print(f"\n验证完成:")
print(f"最佳实际盈利: ${best_validation['actual_profit']:.2f}")
print(f"预测盈利: ${best_validation['predicted_profit']:.2f}")
print(f"预测误差: ${best_validation['prediction_error']:.2f}")
return validation_results
def run_ml_optimizer():
"""运行机器学习优化器"""
try:
# ML优化器配置
EXPLORATION_SIZE = 100 # 第一阶段随机探索数量
PREDICTION_SIZE = 1000 # 第二阶段预测数量
VALIDATION_TOP_N = 20 # 第三阶段验证数量
print("=" * 60)
print("机器学习优化器启动")
print("=" * 60)
print(f"第一阶段: 随机探索 {EXPLORATION_SIZE} 组参数")
print(f"第二阶段: 训练预测模型")
print(f"第三阶段: 预测筛选 {PREDICTION_SIZE} 组参数,验证前 {VALIDATION_TOP_N}")
# 加载历史数据
df_historical_data = load_historical_data()
print(f"历史数据加载完成,数据量: {len(df_historical_data)}")
# 第一阶段:随机探索
exploration_data = phase1_exploration(df_historical_data, EXPLORATION_SIZE)
# 第二阶段:模型训练
model = phase2_model_training(exploration_data)
# 第三阶段:预测筛选和验证
validation_results = phase3_prediction_and_validation(
model, df_historical_data, exploration_data, PREDICTION_SIZE, VALIDATION_TOP_N
)
# 最终结果
best_result = validation_results[0]
best_params_array = best_result['params']
best_profit = best_result['actual_profit']
# 解析最佳参数
parsed_best_params = {}
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
param_value = best_params_array[idx]
if param_def['type'] == 'int':
parsed_best_params[param_def['name']] = int(round(param_value))
else:
parsed_best_params[param_def['name']] = param_value
# 输出结果
print("\n" + "=" * 60)
print("机器学习优化完成")
print("=" * 60)
print(f"最佳盈利: ${best_profit:.2f}")
print(f"预测盈利: ${best_result['predicted_profit']:.2f}")
print(f"预测误差: ${best_result['prediction_error']:.2f}")
# 显示最佳参数
print("\n=== 最佳参数组合 ===")
print("\n策略参数:")
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') not in ['weight', 'signal', 'risk', 'market_state', 'trend_weights', 'trend_thresholds', 'confidence']:
print(f" {param_def['name']}: {parsed_best_params[param_def['name']]}")
print("\n策略权重:")
weight_sum = 0
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') == 'weight':
strategy_name = param_def['name'].replace('weight_', '')
weight_value = parsed_best_params[param_def['name']]
weight_sum += weight_value
print(f" {strategy_name}: {weight_value:.3f}")
print(f"\n权重总和: {weight_sum:.3f}")
# 保存结果(与原优化器格式相同)
save_ml_optimization_results(parsed_best_params, best_profit, exploration_data, validation_results)
return parsed_best_params, best_profit
except Exception as e:
print(f"机器学习优化器运行出错: {e}")
import traceback
print(traceback.format_exc())
sys.exit(1)
def save_ml_optimization_results(best_params, best_fitness, exploration_data, validation_results):
"""保存机器学习优化结果"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 创建结果数据结构
results = {
'optimization_info': {
'timestamp': timestamp,
'method': 'machine_learning',
'best_fitness': float(best_fitness),
'exploration_size': len(exploration_data),
'prediction_size': 1000,
'validation_top_n': len(validation_results),
'random_seed': SEED
},
'best_parameters': best_params,
'exploration_results': exploration_data[:50], # 保存前50个探索结果
'validation_results': validation_results
}
# 保存为JSON文件
json_filename = f"ml_optimization_results_{timestamp}.json"
with open(json_filename, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n机器学习优化结果已保存到: {json_filename}")
# 保存为CSV文件(只保存最佳参数)
csv_filename = f"ml_optimization_best_params_{timestamp}.csv"
with open(csv_filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Parameter', 'Value', 'Type'])
for param_def in PARAMETER_DEFINITIONS:
param_name = param_def['name']
param_value = best_params[param_name]
param_type = param_def['type']
strategy_type = param_def.get('strategy', 'unknown')
writer.writerow([param_name, param_value, f"{param_type}_{strategy_type}"])
print(f"最佳参数已保存到: {csv_filename}")
# 生成可读的文本报告
txt_filename = f"ml_optimization_report_{timestamp}.txt"
with open(txt_filename, 'w', encoding='utf-8') as f:
f.write("=" * 80 + "\n")
f.write("机器学习优化结果报告\n")
f.write("=" * 80 + "\n")
f.write(f"优化时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"优化方法: 随机探索 + 模型预测 + 验证筛选\n")
f.write(f"最佳适应度 (总盈亏): ${best_fitness:.2f}\n")
f.write(f"探索样本数: {len(exploration_data)}\n")
f.write(f"预测样本数: 1000\n")
f.write(f"验证样本数: {len(validation_results)}\n")
f.write(f"随机种子: {SEED}\n")
f.write("\n")
f.write("优化阶段说明:\n")
f.write("-" * 40 + "\n")
f.write("1. 随机探索: 生成100组随机参数进行实际回测\n")
f.write("2. 模型训练: 使用随机森林模型拟合参数与盈利关系\n")
f.write("3. 预测筛选: 预测1000组参数,选择前20组进行验证\n")
f.write("\n")
f.write("最佳参数组合:\n")
f.write("-" * 40 + "\n")
# 分组显示参数
f.write("\n【策略参数】\n")
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') not in ['weight', 'signal', 'risk', 'market_state', 'trend_weights', 'trend_thresholds', 'confidence']:
param_name = param_def['name']
param_value = best_params[param_name]
f.write(f" {param_name}: {param_value}\n")
f.write("\n【策略权重】\n")
weight_sum = 0
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') == 'weight':
param_name = param_def['name']
param_value = best_params[param_name]
strategy_name = param_name.replace('weight_', '')
weight_sum += param_value
f.write(f" {strategy_name}: {param_value:.3f}\n")
f.write(f" 权重总和: {weight_sum:.3f}\n")
f.write("\n" + "=" * 80 + "\n")
f.write("验证结果统计\n")
f.write("=" * 80 + "\n")
validation_profits = [r['actual_profit'] for r in validation_results]
prediction_errors = [r['prediction_error'] for r in validation_results]
f.write(f"验证平均盈利: ${np.mean(validation_profits):.2f}\n")
f.write(f"验证最佳盈利: ${np.max(validation_profits):.2f}\n")
f.write(f"验证最差盈利: ${np.min(validation_profits):.2f}\n")
f.write(f"平均预测误差: ${np.mean(prediction_errors):.2f}\n")
f.write(f"最大预测误差: ${np.max(prediction_errors):.2f}\n")
print(f"详细报告已保存到: {txt_filename}")
except Exception as e:
print(f"保存机器学习优化结果时出错: {e}")
import traceback
print(traceback.format_exc())
if __name__ == "__main__":
run_ml_optimizer()
Binary file not shown.
+150
View File
@@ -0,0 +1,150 @@
import pandas as pd
import numpy as np
import os
import json
from datetime import datetime
# --- 本地模块导入 ---
# 导入波浪理论策略以使用其指标计算和状态判断逻辑
from strategies.wave_theory import WaveTheoryStrategy
# 导入机器学习优化器的核心函数
# (注意:我们可能需要对optimizer_ml.py稍作修改,使其核心功能可以被调用)
from optimizer_ml import run_optimizer as run_ml_optimizer, load_historical_data
# --- 配置 ---
DATA_FILE = "full_historical_data.parquet"
CLASSIFIED_DATA_FILE = "full_historical_data_classified.parquet"
RESULTS_FILE = "regime_optimal_params.json"
# --- 核心功能 ---
def classify_market_regimes(df):
"""
使用WaveTheoryStrategy中的逻辑为整个数据集打上市场状态标签
返回一个带有 'regime' 列的 DataFrame
"""
print("--- 开始为数据集分类市场状态 ---")
# 使用默认参数实例化策略,我们只借用它的指标计算能力
# 这里的参数值不影响状态判断的逻辑本身
strategy = WaveTheoryStrategy(data_provider=None, symbol="XAUUSD", timeframe=None)
# 1. 计算所有需要的指标
# 我们需要确保传入一个副本,避免修改原始df
df_with_indicators = strategy._calculate_indicators(df.copy())
# 2. 逐行判断市场状态
regimes = []
# 使用.values可以稍微加速循环
adx_values = df_with_indicators['adx'].values
ema_short_values = df_with_indicators['ema_short'].values
ema_medium_values = df_with_indicators['ema_medium'].values
ema_long_values = df_with_indicators['ema_long'].values
# 从实例中获取阈值
adx_threshold = strategy.adx_threshold
for i in range(len(df_with_indicators)):
# 处理NaN值的情况,在数据初期指标未生成时,设为None
if np.isnan(adx_values[i]) or np.isnan(ema_short_values[i]) or np.isnan(ema_medium_values[i]) or np.isnan(ema_long_values[i]):
regimes.append(None)
continue
# 判断震荡
# 注意:原始逻辑中还有一个range_pct的判断,这里为简化,暂时只用ADX和均线,后续可完善
if adx_values[i] < adx_threshold:
regimes.append("Ranging")
# 判断上涨趋势
elif ema_short_values[i] > ema_medium_values[i] > ema_long_values[i]:
regimes.append("Uptrend")
# 其他情况视为下跌趋势
else:
regimes.append("Downtrend")
df_with_indicators['regime'] = regimes
# 打印各类别的统计信息
print("\n市场状态分类统计:")
print(df_with_indicators['regime'].value_counts())
# 删除那些没有成功分类的行
df_with_indicators.dropna(subset=['regime'], inplace=True)
print(f"\n去除无法分类的初期数据后,剩余总行数: {len(df_with_indicators)}")
return df_with_indicators
def run_regime_based_optimization():
"""
主函数加载数据分类状态并为每个状态分别运行优化器
"""
# 1. 加载或生成带有市场状态标签的数据
if os.path.exists(CLASSIFIED_DATA_FILE):
print(f"正在从已分类的数据文件 {CLASSIFIED_DATA_FILE} 加载...")
df_classified = pd.read_parquet(CLASSIFIED_DATA_FILE)
print(f"已分类数据加载完成,共 {len(df_classified)} 条记录。")
else:
print(f"未找到已分类的数据文件,将执行初次分类...")
if not os.path.exists(DATA_FILE):
print(f"错误: 原始数据文件 {DATA_FILE} 不存在。请先运行 data_downloader.py。")
return
print(f"正在从 {DATA_FILE} 加载原始数据...")
full_df = pd.read_parquet(DATA_FILE)
print(f"原始数据加载完成,共 {len(full_df)} 条记录。")
# 为整个数据集打上状态标签
df_classified = classify_market_regimes(full_df)
# 保存已分类的数据以备将来使用
print(f"正在将分类后的数据保存到 {CLASSIFIED_DATA_FILE}...")
try:
df_classified.to_parquet(CLASSIFIED_DATA_FILE)
print("保存完成。")
except Exception as e:
print(f"保存已分类数据时出错: {e}")
# 3. 按状态分组,并分别进行优化
all_regime_params = {}
regimes_to_optimize = ['Uptrend', 'Downtrend', 'Ranging']
for regime in regimes_to_optimize:
print(f"\n{'='*80}\n--- 开始为【{regime}】市场状态进行参数优化 ---\n{'='*80}")
regime_df = df_classified[df_classified['regime'] == regime].copy()
if len(regime_df) < 5000: # 如果某个状态的数据太少,优化可能无意义
print(f"警告: {regime} 状态的数据量过少 ({len(regime_df)}条),跳过优化。")
continue
# 调用ML优化器,并把特定状态的数据传递给它
# *** 注意:这需要我们修改 optimizer_ml.py 中的 run_optimizer 函数,使其能接收一个DataFrame作为参数 ***
# *** 目前,我们先假设这个修改已经完成 ***
try:
best_params, best_sharpe = run_ml_optimizer(regime_df)
all_regime_params[regime] = {
'best_sharpe': best_sharpe,
'best_parameters': best_params
}
print(f"\n---【{regime}】状态优化完成!最佳夏普比率: {best_sharpe:.4f} ---")
except Exception as e:
print(f"{regime} 状态优化时发生严重错误: {e}")
import traceback
print(traceback.format_exc())
# 4. 保存最终结果
if all_regime_params:
print(f"\n{'='*80}\n--- 所有市场状态优化全部完成!---\n{'='*80}")
print("最终结果:")
print(json.dumps(all_regime_params, indent=2))
with open(RESULTS_FILE, 'w', encoding='utf-8') as f:
json.dump(all_regime_params, f, ensure_ascii=False, indent=4)
print(f"\n结果已保存到: {os.path.abspath(RESULTS_FILE)}")
else:
print("\n没有完成任何状态的优化。")
if __name__ == "__main__":
run_regime_based_optimization()
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -1,4 +1,5 @@
MetaTrader5
pandas
deap
tqdm
tqdm
plotly
Binary file not shown.
+142 -168
View File
@@ -1,16 +1,16 @@
import sys
import os
import pandas as pd
import json
from datetime import datetime
from tqdm import tqdm
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__name__)))
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from core.data_providers import BacktestDataProvider
from core.risk import RiskController
from execution.dynamic_weights import DynamicWeightManager # Still needed for strategy list
from config import SYMBOL, TIMEFRAME, BACKTEST_COUNT, BACKTEST_START_DATE, BACKTEST_END_DATE, USE_DATE_RANGE, BACKTEST_CONFIG, INITIAL_CAPITAL, SIGNAL_THRESHOLDS, DEFAULT_WEIGHTS
from config import SYMBOL, TIMEFRAME, BACKTEST_COUNT, BACKTEST_START_DATE, BACKTEST_END_DATE, USE_DATE_RANGE, BACKTEST_CONFIG, INITIAL_CAPITAL
from logger import logger
# Import all strategy classes
@@ -25,9 +25,8 @@ from strategies.turtle import TurtleStrategy
from strategies.daily_breakout import DailyBreakoutStrategy
from strategies.wave_theory import WaveTheoryStrategy
def run_full_backtest():
"""完整的、独立的、已重构的回测流程"""
"""完整的、独立的、已重构的回测流程 (支持市场状态)"""
# 1. 加载数据
from core.utils import get_rates, initialize, shutdown
initialize()
@@ -41,189 +40,164 @@ def run_full_backtest():
logger.error("未能获取历史数据,回测终止。")
return
logger.info(f"实际获取数据量: {len(rates)} 条 (请求: {BACKTEST_COUNT} 条)")
# 如果实际获取的数据量超过配置,则截取配置的数据量
if len(rates) > BACKTEST_COUNT:
rates = rates[-BACKTEST_COUNT:] # 取最新的数据
logger.info(f"截取数据量为: {len(rates)}")
df = pd.DataFrame(rates)
df.set_index(pd.to_datetime(df['time'], unit='s'), inplace=True)
# 2. 初始化组件 (data_provider, risk_controller)
data_provider = BacktestDataProvider(df, initial_equity=INITIAL_CAPITAL)
risk_controller = RiskController(data_provider, trade_direction=BACKTEST_CONFIG['trade_direction'])
# weight_manager is not directly used for signal generation in this new architecture,
# but its strategy_blueprints can be used to instantiate strategies.
# 3. 策略实例化和信号预生成 (NEW STAGE 1)
logger.info("开始预生成所有策略信号...")
# Define strategy blueprints directly here or get from DynamicWeightManager if it's refactored
# For now, let's define them directly as DynamicWeightManager is for real-time dynamic weights.
strategy_blueprints = {
'ma_cross': (MACrossStrategy, {}),
'rsi': (RSIStrategy, {}),
'bollinger': (BollingerStrategy, {}),
'mean_reversion': (MeanReversionStrategy, {}),
'momentum_breakout': (MomentumBreakoutStrategy, {}),
'macd': (MACDStrategy, {}),
'kdj': (KDJStrategy, {}),
'turtle': (TurtleStrategy, {}),
'daily_breakout': (DailyBreakoutStrategy, {}),
'wave_theory': (WaveTheoryStrategy, {})
}
# 2. 加载市场状态参数
try:
with open('regime_optimal_params.json', 'r') as f:
regime_params = json.load(f)
logger.info("成功加载市场状态参数文件。")
except FileNotFoundError:
logger.error("错误: regime_optimal_params.json 未找到。请先运行 regime_optimizer.py。")
return
# 3. 市场状态分类
logger.info("开始为整个数据集分类市场状态...")
regime_detector = WaveTheoryStrategy(None, SYMBOL, TIMEFRAME)
df_with_indicators = regime_detector._calculate_indicators(df.copy())
regimes = []
adx_values = df_with_indicators['adx'].values
ema_short_values = df_with_indicators['ema_short'].values
ema_medium_values = df_with_indicators['ema_medium'].values
ema_long_values = df_with_indicators['ema_long'].values
adx_threshold = regime_detector.adx_threshold
for i in range(len(df_with_indicators)):
if pd.isna(adx_values[i]) or pd.isna(ema_short_values[i]):
regimes.append("Ranging")
elif adx_values[i] < adx_threshold:
regimes.append("Ranging")
elif ema_short_values[i] > ema_medium_values[i] > ema_long_values[i]:
regimes.append("Uptrend")
else:
regimes.append("Downtrend")
df['regime'] = regimes
logger.info("市场状态分类完成。")
logger.info(df['regime'].value_counts())
# 4. 分状态生成信号
logger.info("开始分状态生成所有策略信号...")
all_signals_df = pd.DataFrame(index=df.index)
# Map strategy class names to config keys for weights
strategy_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"
strategy_classes = {
"MACrossStrategy": MACrossStrategy,
"RSIStrategy": RSIStrategy,
"BollingerStrategy": BollingerStrategy,
"MeanReversionStrategy": MeanReversionStrategy,
"MomentumBreakoutStrategy": MomentumBreakoutStrategy,
"MACDStrategy": MACDStrategy,
"KDJStrategy": KDJStrategy,
"TurtleStrategy": TurtleStrategy,
"DailyBreakoutStrategy": DailyBreakoutStrategy,
"WaveTheoryStrategy": WaveTheoryStrategy
}
strategies_for_backtest = []
for name, (strategy_class, params) in strategy_blueprints.items():
# Pass None for data_provider, symbol, timeframe as run_backtest only needs df
# But strategy __init__ expects them. So, pass dummy values.
strategies_for_backtest.append(strategy_class(None, SYMBOL, TIMEFRAME, **params))
for strategy in tqdm(strategies_for_backtest, desc="Generating Strategy Signals"):
if hasattr(strategy, 'run_backtest') and callable(getattr(strategy, 'run_backtest')):
try:
strategy_signals = strategy.run_backtest(df.copy()) # Pass a copy to avoid modifying original df
if strategy_signals is not None:
all_signals_df[strategy.name] = strategy_signals
logger.info(f"策略 {strategy.name} 信号生成成功")
else:
logger.warning(f"策略 {strategy.name} 返回空信号")
except Exception as e:
logger.error(f"策略 {strategy.name} 执行失败: {str(e)}")
# 为失败的策略创建全0信号序列
all_signals_df[strategy.name] = pd.Series(0, index=df.index)
continue
else:
logger.warning(f"策略 {strategy.name} 没有实现 'run_backtest' 方法,将被跳过。")
# 组合信号
logger.info("开始组合策略信号...")
combined_weighted_signals = pd.Series(0, index=df.index)
if not all_signals_df.empty:
weighted_signals_list = []
for col_name in all_signals_df.columns:
# Extract strategy class name from column name (e.g., 'MACrossStrategy')
strategy_class_name = col_name
config_key = strategy_name_to_config_key.get(strategy_class_name)
if config_key and config_key in DEFAULT_WEIGHTS:
weight = DEFAULT_WEIGHTS[config_key]
weighted_signals_list.append(all_signals_df[col_name] * weight)
else:
logger.warning(f"未找到策略 {strategy_class_name} 的默认权重,使用权重1.0。")
weighted_signals_list.append(all_signals_df[col_name] * 1.0)
if weighted_signals_list:
combined_weighted_signals = pd.concat(weighted_signals_list, axis=1).sum(axis=1)
else:
logger.warning("没有生成任何加权信号。")
# 应用阈值得到最终信号
final_signals = pd.Series(0, index=df.index)
buy_threshold = SIGNAL_THRESHOLDS.get("buy_threshold", 1.0)
sell_threshold = SIGNAL_THRESHOLDS.get("sell_threshold", -1.0)
final_signals[combined_weighted_signals > buy_threshold] = 1
final_signals[combined_weighted_signals < sell_threshold] = -1
logger.info("信号预生成和组合完成。")
# 4. 回测主循环 (NEW STAGE 2)
logger.info(f"回测主循环开始... 数据量: {len(df)}")
# Reset data_provider's internal index to 0 for the loop
data_provider.current_index = 0
for i in tqdm(range(len(df)), desc=f"Backtesting ({len(df)} bars)"):
try:
# Get current price from the data_provider (which uses its internal index)
current_price = data_provider.get_current_price(SYMBOL)
if not current_price:
logger.warning(f"无法获取当前价格在索引 {i},跳过。")
continue
# Get the pre-generated signal for the current bar
current_signal = final_signals.iloc[i]
direction = None
if current_signal == 1:
direction = "buy"
elif current_signal == -1:
direction = "sell"
if direction:
risk_controller.process_trading_signal(direction, current_price, abs(current_signal))
# Monitor and update positions
risk_controller.monitor_positions(current_price)
# Advance data provider to the next time step
data_provider.tick()
except Exception as e:
logger.error(f"回测循环中在索引 {i} 发生错误: {str(e)}")
# 继续下一个bar,不中断整个回测
for regime in ['Uptrend', 'Downtrend', 'Ranging']:
logger.info(f"--- 为 {regime} 状态生成信号 ---")
regime_df = df[df['regime'] == regime]
if regime_df.empty:
logger.info(f"{regime} 状态没有数据,跳过。")
continue
# 5. 结束和报告 (Keep as is)
params = regime_params.get(regime, {}).get('best_parameters', {})
if not params:
logger.warning(f"未找到 {regime} 的参数,将无法为该状态生成信号。")
continue
for strat_name, strat_class in strategy_classes.items():
instance = strat_class(None, SYMBOL, TIMEFRAME)
strat_params = {}
prefix_map = {
'MACrossStrategy': 'ma_cross_',
'RSIStrategy': 'rsi_',
'BollingerStrategy': 'bollinger_',
'MACDStrategy': 'macd_',
'MeanReversionStrategy': 'mean_reversion_',
'MomentumBreakoutStrategy': 'momentum_breakout_',
'KDJStrategy': 'kdj_',
'TurtleStrategy': 'turtle_',
'DailyBreakoutStrategy': 'daily_breakout_',
'WaveTheoryStrategy': 'wave_'
}
prefix = prefix_map[strat_name]
for p_name, p_val in params.items():
if p_name.startswith(prefix):
param_name = p_name.replace(prefix, '')
if strat_name == 'WaveTheoryStrategy' and p_name == 'wave_period':
param_name = 'wave_period'
strat_params[param_name] = p_val
instance.set_params(strat_params)
try:
signals = instance.run_backtest(regime_df.copy())
if signals is not None:
all_signals_df.loc[regime_df.index, strat_name] = signals
except Exception as e:
logger.error(f"策略 {strat_name}{regime} 状态下执行失败: {e}")
all_signals_df.fillna(0, inplace=True)
# 5. 组合信号
logger.info("开始组合策略信号...")
final_signals = pd.Series(0.0, index=df.index)
for regime in ['Uptrend', 'Downtrend', 'Ranging']:
regime_df_indices = df[df['regime'] == regime].index
if regime_df_indices.empty: continue
params = regime_params.get(regime, {}).get('best_parameters', {})
weights = {k.replace('weight_', ''): v for k, v in params.items() if k.startswith('weight_')}
buy_threshold = params.get('buy_threshold', 1.5)
sell_threshold = params.get('sell_threshold', -1.5)
regime_signals = all_signals_df.loc[regime_df_indices]
weighted_sum = pd.Series(0.0, index=regime_signals.index)
for strat_name, weight in weights.items():
if strat_name in regime_signals.columns:
weighted_sum += regime_signals[strat_name] * weight
# BUG FIX: Use indices to avoid alignment errors
buy_indices = weighted_sum[weighted_sum > buy_threshold].index
sell_indices = weighted_sum[weighted_sum < sell_threshold].index
final_signals.loc[buy_indices] = 1
final_signals.loc[sell_indices] = -1
# 6. 回测主循环
logger.info("回测主循环开始...")
data_provider = BacktestDataProvider(df, initial_equity=INITIAL_CAPITAL)
risk_controller = RiskController(data_provider, trade_direction=BACKTEST_CONFIG['trade_direction'])
for i in tqdm(range(len(df)), desc="Backtesting"):
current_price = data_provider.get_current_price(SYMBOL)
if not current_price: continue
current_signal = final_signals.iloc[i]
direction = "buy" if current_signal == 1 else "sell" if current_signal == -1 else None
if direction:
risk_controller.process_trading_signal(direction, current_price, abs(current_signal))
risk_controller.monitor_positions(current_price)
data_provider.tick()
# 7. 结束和报告
logger.info("回测完成,生成性能报告...")
summary = risk_controller.position_manager.get_trade_summary()
# 打印报告
logger.info("=" * 80)
logger.info("回测性能报告")
logger.info(f"总交易次数: {summary.get('total_trades', 0)}")
logger.info(f"胜率: {summary.get('win_rate', 0):.2f}%")
logger.info(f"总盈亏: ${summary.get('total_profit_loss', 0):.2f}")
logger.info("=" * 80)
# 保存交易记录
try:
risk_controller.position_manager.save_trade_history("backtest_trades")
logger.info("交易记录保存成功")
except Exception as e:
logger.error(f"保存交易记录失败: {str(e)}")
# 尝试手动保存
try:
import json
trades = risk_controller.position_manager.closed_trades
if trades:
with open("backtest_trades_manual.json", 'w', encoding='utf-8') as f:
json.dump(trades, f, ensure_ascii=False, indent=2, default=str)
logger.info("手动保存交易记录到 backtest_trades_manual.json")
except Exception as e2:
logger.error(f"手动保存交易记录也失败: {str(e2)}")
risk_controller.position_manager.save_trade_history("backtest_trades")
def main():
print("=" * 60)
print("MetaTrader 5 智能交易系统 - 回测")
print("MetaTrader 5 智能交易系统 - 回测 (市场状态模式)")
print("=" * 60)
try:
run_full_backtest()
print("\n回测完成!")
except Exception as e:
import traceback
print(f"\n回测出错: {e}")
traceback.print_exc()
run_full_backtest()
if __name__ == "__main__":
main()
main()
+9
View File
@@ -15,6 +15,15 @@ class BaseStrategy:
self.timeframe = timeframe
self.name = self.__class__.__name__
def set_params(self, params):
"""动态设置策略参数"""
for key, value in params.items():
if hasattr(self, key):
setattr(self, key, value)
logger.debug(f"{self.name}: 参数 '{key}' 已更新为 {value}")
else:
logger.warning(f"{self.name}: 尝试设置不存在的参数 '{key}'")
def _log_signal(self, signal, reason=None):
"""记录信号日志"""
if signal != 0:
+6 -3
View File
@@ -38,13 +38,16 @@ class DailyBreakoutStrategy(BaseStrategy):
def run_backtest(self, df):
df = df.copy()
df['time'] = pd.to_datetime(df['time'], unit='s')
df['date'] = df['time'].dt.date
# BUG FIX: The 'time' column does not exist in a properly formed dataframe.
# Time information should be derived from the DatetimeIndex.
df['date'] = df.index.date
# LOGIC FIX: daily_lows should be the minimum of the day, not the maximum.
daily_highs = df.groupby('date')['high'].transform('max')
daily_lows = df.groupby('date')['low'].transform('max')
daily_lows = df.groupby('date')['low'].transform('min')
signals = pd.Series(0, index=df.index)
# Signal when close breaks yesterday's high/low
signals[df['close'] > daily_highs.shift(1)] = 1
signals[df['close'] < daily_lows.shift(1)] = -1
return signals
+95
View File
@@ -0,0 +1,95 @@
import pandas as pd
import numpy as np
import plotly.graph_objects as go
import os
# 从 regime_optimizer 脚本中导入状态分类函数
from regime_optimizer import classify_market_regimes
# --- 配置 ---
DATA_FILE = "full_historical_data.parquet"
OUTPUT_HTML_FILE = "regime_visualization.html"
# --- 主函数 ---
def create_regime_candlestick_chart():
"""
加载数据分类市场状态并生成一个按状态染色的交互式K线图
"""
# 1. 加载数据
if not os.path.exists(DATA_FILE):
print(f"错误: 数据文件 {DATA_FILE} 不存在。请先运行 data_downloader.py。")
return
print(f"正在从 {DATA_FILE} 加载数据...")
full_df = pd.read_parquet(DATA_FILE)
print(f"数据加载完成,共 {len(full_df)} 条记录。")
# 2. 分类市场状态
# 这个函数会返回一个带有 'regime' 列的新DataFrame
df_classified = classify_market_regimes(full_df)
print("\n--- 开始生成交互式K线图 ---")
# 3. 创建Plotly图表对象
fig = go.Figure()
# 4. 为每种市场状态分别创建一个K线图层 (Trace)
# 这是处理大数据和分类染色的最高效方法
# 我们通过将不属于当前状态的数据设置为NaN来“隐藏”它们,从而只绘制我们想要的K线
regime_colors = {
"Uptrend": "red",
"Downtrend": "green",
"Ranging": "black"
}
for regime, color in regime_colors.items():
print(f"正在为 {regime} 状态创建图层...")
# 创建一个临时DataFrame用于绘图
df_trace = df_classified.copy()
# 将不属于当前状态的K线数据设置为空值(NaN)
df_trace.loc[df_trace['regime'] != regime, ['open', 'high', 'low', 'close']] = np.nan
# 添加K线图层
fig.add_trace(go.Candlestick(
x=df_trace.index,
open=df_trace['open'],
high=df_trace['high'],
low=df_trace['low'],
close=df_trace['close'],
name=regime,
increasing_line_color=color, # 上涨部分
decreasing_line_color=color # 下跌部分
))
# 5. 更新图表布局和样式
print("正在配置图表样式...")
fig.update_layout(
title={
'text': "市场状态可视化 (XAUUSD - M1)",
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'
},
xaxis_title="日期",
yaxis_title="价格",
legend_title="市场状态",
template="plotly_dark", # 使用深色主题
xaxis_rangeslider_visible=False # **关键:为提升性能,禁用底部范围滑块**
)
# 6. 保存为HTML文件
try:
fig.write_html(OUTPUT_HTML_FILE)
print(f"\n--- 图表生成成功! ---")
print(f"已保存到: {os.path.abspath(OUTPUT_HTML_FILE)}")
print("请用您的浏览器打开此文件以进行交互式分析。")
except Exception as e:
print(f"保存HTML文件时出错: {e}")
if __name__ == "__main__":
create_regime_candlestick_chart()