mirror of
https://github.com/silencesdg/mt5_python_ea_suite.git
synced 2026-07-29 19:57:43 +00:00
add ml
This commit is contained in:
@@ -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`: 实际成交量
|
||||
|
||||
## 🚦 运行模式
|
||||
|
||||
|
||||
@@ -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, # 中等置信度阈值
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
@@ -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
@@ -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.
@@ -0,0 +1,24 @@
|
||||
按“机器学习/模型预测优化”思路来改,结构可以这样调整:
|
||||
|
||||
---
|
||||
|
||||
## 改法核心
|
||||
|
||||
1. 保留 **回测调用逻辑**(原来 GA 的 `evaluate()` 或 `fitness()` 部分),因为它是核心的盈利数计算。
|
||||
2. 去掉 GA 的选择/交叉/变异部分,改成:
|
||||
|
||||
* **第一阶段(探索)**:随机生成 N 组参数 → 跑回测 → 存成 `(params, profit)`
|
||||
* **第二阶段(利用)**:训练一个预测模型(例如 `RandomForestRegressor`)来拟合 `(params → profit)`
|
||||
* **第三阶段(优化)**:用模型快速评估大量随机参数 → 挑预测盈利最高的 Top N → 实际回测确认
|
||||
|
||||
---
|
||||
|
||||
在这个 `optimizer.py` 里:
|
||||
|
||||
* 保留现有的 **参数定义、回测调用、指标计算**
|
||||
* 替换 GA 循环为 **“随机探索 + 模型预测筛选”**
|
||||
* 把结果输出成和现在 GA 一样的格式(方便和旧版本对比)
|
||||
|
||||
这样就能直接运行,不需要重写项目结构。
|
||||
|
||||
保留现有所有参数配置和回测入口,只替换优化器核心逻辑。
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
@@ -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.
@@ -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
@@ -1,4 +1,5 @@
|
||||
MetaTrader5
|
||||
pandas
|
||||
deap
|
||||
tqdm
|
||||
tqdm
|
||||
plotly
|
||||
Binary file not shown.
+142
-168
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user