重构项目架构,新增 MT5 代理服务

- 重构核心模块:DataProvider 依赖注入、RiskController 门面、信号注册表
- 新增 FastAPI 代理服务 (run/server.py),支持局域网远程调用 MT5
- 新增 RemoteDataProvider + AttrDict,远端无缝替代 LiveDataProvider
- 新增序列化模块,MT5 对象转 JSON 兼容格式
- 重构入口点至 run/ 包,支持 python -m run.realtime/server/backtest/optimize
- 更新 CLAUDE.md 文档

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
silencesdg
2026-05-11 12:00:45 +08:00
parent e8226edd96
commit 4cb4f4a15e
46 changed files with 2993 additions and 2279 deletions
+83 -80
View File
@@ -4,103 +4,106 @@
## 项目概述
这是一个基于Python的MetaTrader 5智能交易系统(EA)套件,用于量化交易策略。系统通过加权投票和阈值过滤组合多个交易策略,生成稳健的交易决策
基于Python的MetaTrader 5智能交易系统(EA)套件,集成10种量化策略,通过加权投票和阈值过滤组合信号,支持动态权重管理(根据市场状态调整策略权重)
## 架构设计
## 实际入口点
### 核心组件
`main.py` 为空壳,实际入口为:
- **主入口**: `main.py` 提供三种执行模式:
- `run_backtest()`: 使用当前配置权重进行单次回测
- `run_optimizer()`: 遗传算法优化寻找最佳权重
- `run_realtime()`: 与MT5集成的实时交易
- **配置文件**: `config.py` 包含:
- `STRATEGIES`: (策略实例, 权重) 元组列表
- 交易参数(品种、时间周期、初始资金)
- **回测引擎**: `backtest.py` 处理:
- 历史数据策略执行
- 使用加权和的信号组合
- 防止未来函数的收益计算
- **遗传优化器**: `optimizer.py`:
- 使用DEAP库进行遗传算法
- 优化策略权重以获得最大收益
- 支持并行处理提高性能
- **工具函数**: `utils.py` 提供MT5连接管理和交易功能
- **日志系统**: `logger.py` 处理控制台/文件双日志输出,支持UTF-8编码
### 策略架构
`strategies/` 目录中的所有策略都遵循一致的模式:
```python
class Strategy:
def __init__(self): # 初始化参数
def _calculate_indicators(self, df): # 计算技术指标
def generate_signal(self): # 实时交易信号生成
def run_backtest(self, df): # 回测信号生成
```bash
python start_backtest.py # 回测模式
python start_realtime.py # 实时交易(默认模拟,需输入YES确认实盘)
python optimizer.py # 遗传算法参数优化
```
## 核心架构
### 依赖注入设计
`core/data_providers.py` 定义了 `DataProvider` 抽象基类(9个抽象方法),三种实现:
- `LiveDataProvider` — 真实MT5 API调用
- `DryRunDataProvider` — 模拟交易(委托LiveDataProvider获取价格,不发送真实订单)
- `BacktestDataProvider` — 回测(维护DataFrame + `current_index`,通过 `tick()` 推进)
所有策略和执行模块接收 `DataProvider` 接口,不直接调用MT5。这是理解整个系统解耦的关键。
### 风险管理门面
`core/risk/__init__.py` 中的 `RiskController` 是门面类,组合了:
- `PositionManager` (`position_manager.py`) — 开仓、监控、止损/止盈/追踪止损/时间退出、资金管理
- `MarketStateAnalyzer` (`market_state.py`) — 四维度趋势检测(价格突破、成交量确认、动量、均线),返回 `("uptrend"/"downtrend"/"ranging"/"none", confidence)`
### 动态权重系统
`execution/dynamic_weights.py` 中的 `DynamicWeightManager`
1. 使用 `MarketStateAnalyzer` 判断当前市场状态
2. 根据状态从 `config.py:MARKET_STATE_WEIGHTS` 获取对应权重
3. 按置信度决定使用市场状态权重还是 `DEFAULT_WEIGHTS`
4. 返回 `[(策略实例, 权重), ...]` 列表供信号组合使用
### 两阶段回测设计
`start_backtest.py` 的回测采用两阶段:
1. **预生成阶段**:遍历所有策略 `run_backtest(df)` 生成信号列,加权求和后通过 `SIGNAL_THRESHOLDS` 阈值过滤得到最终信号序列
2. **模拟执行阶段**:单遍 `iterrows` 逐行推进,通过 `RiskController` 模拟开仓/监控/平仓
### 信号组合逻辑
系统通过以下方式组合策略信号:
1. 加权求和:`sum(信号 * 权重,针对所有策略)`
2. 信号决策:加权和大于0买入,小于0卖出
3. 返回用于回测的组合信号序列
## 开发命令
### 环境设置
```bash
# 创建并激活虚拟环境
python -m venv myenv
# Windows
myenv\Scripts\activate
# macOS/Linux
source myenv/bin/activate
# 安装依赖
pip install -r requirements.txt
```
加权和 = sum(各策略信号 * 权重)
最终信号 = 1 (买入) if 加权和 > buy_threshold (1.5)
-1 (卖出) if 加权和 < sell_threshold (-1.5)
0 (无信号) otherwise
```
### 运行模式
```bash
# 运行单次回测(使用config.py权重)
python main.py
阈值在 `config.py:SIGNAL_THRESHOLDS` 中配置。
# 运行遗传优化(寻找最佳权重)
python main.py # 取消注释 run_optimizer() 调用
## 策略开发规范
# 运行实时交易(使用config.py权重)
python main.py # 取消注释 run_realtime() 调用
所有策略继承 `strategies/base_strategy.py``BaseStrategy`
```python
class BaseStrategy:
def __init__(self, data_provider, symbol, timeframe): # 接收 data_provider 而非直接访问 MT5
def generate_signal(self) -> int: # 实时信号:通过 self.data_provider 获取当前价格
def run_backtest(self, df) -> pd.Series: # 向量化回测:接收 DataFrame,返回 -1/0/1 序列
def _log_signal(self, signal, reason): # 基类提供,记录买卖信号到日志
```
## 关键开发模式
**添加新策略步骤:**
1.`strategies/` 下创建新文件,继承 `BaseStrategy`
2. 实现 `generate_signal()``run_backtest(self, df)`
3.`config.py:STRATEGY_CONFIG` 添加参数字典,在 `config.py:DEFAULT_WEIGHTS``config.py:MARKET_STATE_WEIGHTS` 添加权重
4.`execution/dynamic_weights.py``strategy_blueprints` 列表和 `optimizer.py` 的导入中注册
### 添加新策略
1.`strategies/` 目录创建新文件
2. 实现必需的Strategy类方法
3. 添加导入和策略实例到 `config.py:STRATEGIES`
## 配置系统
### 配置管理
- 策略权重需要从优化器结果手动更新
- 所有交易参数集中在 `config.py`
`config.py` 包含20+个配置字典,核心:
### 错误处理
- 策略执行中的全面异常处理
- 日志文件使用UTF-8编码
- 操作前进行MT5连接验证
| 配置 | 用途 |
|---|---|
| `SYMBOL`, `TIMEFRAME`, `INITIAL_CAPITAL` | 基础交易参数(当前: XAUUSD, M1, 20000 |
| `STRATEGY_CONFIG` | 各策略的参数字典(键名为策略简称) |
| `DEFAULT_WEIGHTS` | 默认策略权重(优化后) |
| `MARKET_STATE_WEIGHTS` | 各市场状态(up/down/ranging)下的策略权重 |
| `SIGNAL_THRESHOLDS` | 买卖信号阈值 |
| `RISK_CONFIG` | 止损/止盈/追踪止损/持仓时间等风控参数 |
| `MARKET_STATE_CONFIG` | 趋势检测参数 |
| `GENETIC_OPTIMIZER_CONFIG` | 遗传算法参数(种群、代数、交叉/变异概率) |
| `BACKTEST_CONFIG` / `REALTIME_CONFIG` | 回测/实盘专项配置 |
### 性能考虑
- 优化器预加载历史数据避免重复I/O
- 遗传算法启用并行处理
- 日志器使用单例模式防止重复处理器
## 依赖
```
MetaTrader5 # MT5 Python API
pandas
deap # 遗传算法框架
tqdm # 进度条
```
## 语言要求
- **所有开发和文档必须使用中文**(根据GEMINI.md
- 错误日志在 `logs/error.log`
- 策略日志在 `logs/strategy.log`
- 所有代码注释、文档、日志必须使用中文
- 日志文件: `logs/strategy.log`, `logs/error.log`
- 日志器使用单例模式(`logger.py`
+81 -96
View File
@@ -1,19 +1,23 @@
# MT5 代理服务配置
SERVER_HOST = "0.0.0.0"
SERVER_PORT = 5555
# 远端客户端配置(迁移到其他电脑时填写 MT5 机器的 IP)
REMOTE_SERVER_HOST = "127.0.0.1"
REMOTE_SERVER_PORT = 5555
# 交易配置
SYMBOL = "XAUUSD"
INTERVAL = 60 #
INITIAL_CAPITAL = 20000 # 初始资金
SPREAD = 32 # 点差(点数),买卖合起来的总共点差成本
SYMBOL = "XAUUSDz"
INITIAL_CAPITAL = 1937 # 初始资金(2026-05-11 实盘余额)
# 时间配置
TIMEFRAME = 1# M1 (1分钟图) - MT5常量值
# 回测时间范围 (格式: "YYYY-MM-DD")
# 注意:这些日期需要确保在MT5服务器上有可用数据
BACKTEST_START_DATE = "2025-05-01"
BACKTEST_END_DATE = "2025-08-01"
# 优化器时间范围 (格式: "YYYY-MM-DD")
# 注意:确保与回测时间不重叠
OPTIMIZER_START_DATE = "2025-04-01"
OPTIMIZER_END_DATE = "2025-05-01"
@@ -22,7 +26,7 @@ USE_DATE_RANGE = False # 设置为False可强制使用数据量模式
# 兼容性配置 (如果日期配置不可用,则使用数据量)
BACKTEST_COUNT = 30000 # 回测数据量
OPTIMIZER_COUNT = 15000 # 优化器数据量
OPTIMIZER_COUNT = 50000 # 优化器数据量
RISK_CONFIG_CONST = {
'enable_time_based_exit': True
@@ -74,23 +78,23 @@ DATA_CONFIG = {
# 遗传算法优化器配置
GENETIC_OPTIMIZER_CONFIG = {
# 算法参数
"population_size": 200, # 种群大小
"generations": 50, # 进化代数
"population_size": 50, # 种群大小
"generations": 10, # 进化代数
"crossover_probability": 0.7, # 交叉概率
"mutation_probability": 0.3, # 变异概率
# 选择算法参数
"tournament_size": 3, # 锦标赛选择大小
# 变异算法参数
"mutation_mu": 0, # 变异均值
"mutation_sigma": 0.1, # 变异标准差
"mutation_indpb": 0.1, # 变异概率(每个基因)
# 并行处理
"enable_multiprocessing": True, # 启用多进程
"processes": None, # 进程数,None表示自动检测
# 输出控制
"verbose": True, # 详细输出
"save_generation_info": True, # 保存代数信息
@@ -103,131 +107,113 @@ GENETIC_OPTIMIZER_CONFIG = {
此处下面所有参数,都将进入优化器进行优化
'''
# 信号阈值配置
# 信号阈值配置(优化器结果 2026-05-105万根M1数据)
SIGNAL_THRESHOLDS = {
"buy_threshold": 1.5, # 买入信号阈值 (优化后)
"sell_threshold": -0.87 # 卖出信号阈值 (优化后)
"buy_threshold": 1.344,
"sell_threshold": -2.980
}
# 风险管理参数
# 风险管理参数(优化器结果 2026-05-105万根M1数据)
RISK_CONFIG = {
"stop_loss_pct": -0.01, # 固定止损:亏损1% (优化后)
"profit_retracement_pct": 0.1, # 利润回撤10%止盈 (优化后)
"min_profit_for_trailing": 0.01, # 追踪止损激活阈值:利润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%则平仓 (优化后)
"stop_loss_pct": -0.046,
"profit_retracement_pct": 0.070,
"min_profit_for_trailing": 0.009,
"take_profit_pct": 0.246,
"max_daily_loss": -0.3,
"max_holding_minutes": 133,
"min_profit_for_time_exit": 0.010,
"cooldown_bars": 30
}
# 市场状态分析参数
MARKET_STATE_CONFIG = {
"trend_period": 44,
"retracement_tolerance": 0.1382775008306345,
"volume_period": 13,
"volume_ma_period": 12,
"trend_period": 24,
"retracement_tolerance": 0.425,
"volume_period": 21,
"volume_ma_period": 12
}
# 策略参数配置 (从优化器中提取的优化参数)
# 策略参数配置(优化器结果 2026-05-105万根M1数据)
STRATEGY_CONFIG = {
# MACrossStrategy 参数
"ma_cross": {
"short_window": 5,
"long_window": 39,
"short_window": 12,
"long_window": 30
},
# RSIStrategy 参数
"rsi": {
"period": 22,
"overbought": 80,
"oversold": 24,
"period": 21,
"overbought": 75,
"oversold": 26
},
# BollingerStrategy 参数
"bollinger": {
"period": 10,
"std_dev": 2.8916513144581044,
"period": 20,
"std_dev": 2.162
},
# MACDStrategy 参数
"macd": {
"fast_ema": 17,
"slow_ema": 22,
"signal_period": 13,
"fast_ema": 16,
"slow_ema": 34,
"signal_period": 12
},
# MeanReversionStrategy 参数
"mean_reversion": {
"period": 40,
"std_dev": 2.917216289407233,
"period": 29,
"std_dev": 2.149
},
# MomentumBreakoutStrategy 参数
"momentum_breakout": {
"period": 27,
"period": 15,
"momentum_period": 17
},
# KDJStrategy 参数
"kdj": {
"period": 6,
"period": 21
},
# TurtleStrategy 参数
"turtle": {
"period": 16,
"period": 42
},
# DailyBreakoutStrategy 参数
"daily_breakout": {
"bars_count": 807,
"bars_count": 746
},
# WaveTheoryStrategy 参数
"wave_theory": {
"ema_short": 3,
"ema_medium": 15,
"ema_long": 36,
"wave_period": 28,
"ema_medium": 16,
"ema_long": 26,
"wave_period": 32,
"range_period": 30,
"adx_period": 21,
"momentum_period": 13,
"range_threshold": -0.02028040971155598,
"adx_threshold": 22,
},
"adx_period": 23,
"momentum_period": 10,
"range_threshold": 0.002,
"adx_threshold": 23
}
}
# 市场趋势判断权重配置
TREND_INDICATOR_WEIGHTS = {
"price_breakout": 0.03552729699860058, # 价格突破权重
"volume_confirmation": 0.2564191814903339, # 成交量确认权重
"momentum oscillator": 0.48690789892001296, # 动量震荡指标权重
"moving_average": 0.3549274308680269, # 移动平均线权重
"price_breakout": -0.0949,
"volume_confirmation": 0.5277,
"momentum oscillator": 0.3677,
"moving_average": 0.1506
}
# 趋势判断阈值
TREND_THRESHOLDS = {
"strong_trend": 0.24063407379490956, # 强趋势阈值
"weak_trend": 0.3708545035763192, # 弱趋势阈值
"volume_spike": 1.7781348694952452, # 成交量突增倍数
"oversold": 29, # 超卖阈值 (RSI)
"overbought": 69, # 超买阈值 (RSI)
"strong_trend": 0.4182,
"weak_trend": 0.2164,
"volume_spike": 1.5843,
"oversold": 24,
"overbought": 80
}
# 动态权重配置(经过优化器优化的最佳权重
# 动态权重配置(优化器结果 2026-05-105万根M1数据
DEFAULT_WEIGHTS = {
"ma_cross": 0.44428771408324463,
"rsi": 1.6518277343219148,
"bollinger": 0.6014474871460991,
"mean_reversion": 0.04509729271515517,
"momentum_breakout": 1.0768245883204248,
"macd": 0.8909161638775971,
"kdj": 1.095784002572773,
"turtle": 1.7942934575029192,
"daily_breakout": 2.4237777692491713,
"wave_theory": 0.7354173414675755,
"ma_cross": 0.809,
"rsi": 1.141,
"bollinger": 0.389,
"mean_reversion": 1.106,
"momentum_breakout": 0.147,
"macd": 1.181,
"kdj": 1.525,
"turtle": 0.559,
"daily_breakout": 1.846,
"wave_theory": 1.361
}
# 市场状态策略权重配置
@@ -273,7 +259,6 @@ MARKET_STATE_WEIGHTS = {
# 市场趋势置信度阈值配置
CONFIDENCE_THRESHOLDS = {
"high_confidence": 0.941049792261965, # 高置信度阈值
"medium_confidence": 0.8881796011658835, # 中等置信度阈值
"high_confidence": 0.8474,
"medium_confidence": 0.4964
}
+6
View File
@@ -0,0 +1,6 @@
from core.data.abc import DataProvider
from core.data.live import LiveDataProvider
from core.data.dryrun import DryRunDataProvider
from core.data.backtest import BacktestDataProvider
from core.data.multi_tf import MultiTimeframeDataStore
from core.data.remote import RemoteDataProvider
+45
View File
@@ -0,0 +1,45 @@
from abc import ABC, abstractmethod
class DataProvider(ABC):
"""数据提供者抽象基类 — 定义所有数据访问的统一接口"""
@property
def is_live(self):
return False
@abstractmethod
def initialize(self):
"""初始化连接"""
@abstractmethod
def shutdown(self):
"""关闭连接"""
@abstractmethod
def get_current_price(self, symbol):
"""获取当前价格: {'bid', 'ask', 'last', 'time'}"""
@abstractmethod
def get_historical_data(self, symbol, timeframe, count, **kwargs):
"""获取历史K线数据"""
@abstractmethod
def get_account_info(self):
"""获取账户信息"""
@abstractmethod
def get_positions(self, symbol):
"""获取当前持仓"""
@abstractmethod
def get_symbol_info(self, symbol):
"""获取品种信息(合约规格等)"""
@abstractmethod
def send_order(self, symbol, order_type, volume):
"""发送订单"""
@abstractmethod
def close_position(self, ticket, symbol, volume):
"""平仓"""
+80
View File
@@ -0,0 +1,80 @@
import numpy as np
from logger import logger
from core.data.abc import DataProvider
from core.data.multi_tf import MultiTimeframeDataStore
from config import SIMULATION_CONFIG
class BacktestDataProvider(DataProvider):
"""回测数据提供者(重写版) — 通过 MultiTimeframeDataStore 支持多周期数据
关键改进:
- get_historical_data() 的 timeframe 参数现在真正生效
- 内部持有 MultiTimeframeDataStore,按需从M1生成更高周期数据
- current_index 推进时各周期数据指针同步
"""
def __init__(self, multi_tf_store: MultiTimeframeDataStore,
initial_equity: float = 10000):
self.multi_tf = multi_tf_store
self.current_index = 0
self.equity = initial_equity
self.simulated_ticket_counter = 0
self.spread = SIMULATION_CONFIG.get("spread", 16)
@property
def total_length(self) -> int:
return self.multi_tf.length
def initialize(self):
return True
def shutdown(self):
pass
def get_account_info(self):
return {'equity': self.equity}
def get_positions(self, symbol):
return []
def get_symbol_info(self, symbol):
return {
'trade_contract_size': SIMULATION_CONFIG['contract_size'],
'volume_step': SIMULATION_CONFIG['volume_step'],
'volume_min': SIMULATION_CONFIG['volume_min'],
'volume_max': SIMULATION_CONFIG['volume_max'],
}
def get_current_price(self, symbol):
if self.current_index >= self.total_length:
return None
row = self.multi_tf.main_df.iloc[self.current_index]
spread_half = self.spread * 0.01 / 2
return {
'bid': row.close - spread_half,
'ask': row.close + spread_half,
'last': row.close,
'time': row.name
}
def get_historical_data(self, symbol, timeframe, count, **kwargs):
"""获取历史数据 — timeframe 参数现在真正生效"""
return self.multi_tf.get_historical_data(
symbol, timeframe, count, self.current_index
)
def send_order(self, symbol, order_type, volume):
self.simulated_ticket_counter += 1
logger.info(f"[回测模式] 下单: {order_type} {volume:.2f}{symbol}")
return {'order': self.simulated_ticket_counter}
def close_position(self, ticket, symbol, volume):
logger.info(f"[回测模式] 平仓: Ticket {ticket}")
return True
def tick(self) -> bool:
if self.current_index < self.total_length - 1:
self.current_index += 1
return True
return False
+62
View File
@@ -0,0 +1,62 @@
import pandas as pd
from logger import logger
from core.data.abc import DataProvider
from core.data.live import LiveDataProvider
from config import SIMULATION_CONFIG
class DryRunDataProvider(DataProvider):
"""纸上交易数据提供者 — 使用实时价格但不发送真实订单"""
def __init__(self, initial_equity=10000, leverage=100):
self.simulated_ticket_counter = 0
self.equity = initial_equity
self.leverage = leverage
self._live_data_source = LiveDataProvider()
self.spread = SIMULATION_CONFIG.get("spread", 16)
def initialize(self):
logger.info("纸上交易模式初始化...")
return self._live_data_source.initialize()
def shutdown(self):
logger.info("纸上交易模式关闭.")
self._live_data_source.shutdown()
def get_current_price(self, symbol):
price_data = self._live_data_source.get_current_price(symbol)
if price_data:
spread_half = self.spread * 0.01 / 2
mid_price = price_data['last']
return {
'bid': mid_price - spread_half,
'ask': mid_price + spread_half,
'last': mid_price,
'time': price_data['time']
}
return None
def get_historical_data(self, symbol, timeframe, count, **kwargs):
return self._live_data_source.get_historical_data(symbol, timeframe, count, **kwargs)
def get_symbol_info(self, symbol):
return self._live_data_source.get_symbol_info(symbol)
def get_account_info(self):
return {'equity': self.equity}
def get_positions(self, symbol):
return []
def send_order(self, symbol, order_type, volume):
self.simulated_ticket_counter += 1
price_data = self.get_current_price(symbol)
price = price_data['last'] if price_data else "N/A"
logger.info(f"[纸上交易] 模拟下单: {order_type} {volume:.2f}{symbol} @ {price}")
return {'order': self.simulated_ticket_counter}
def close_position(self, ticket, symbol, volume):
price_data = self.get_current_price(symbol)
price = price_data['last'] if price_data else "N/A"
logger.info(f"[纸上交易] 模拟平仓: Ticket {ticket} @ {price}")
return True
+172
View File
@@ -0,0 +1,172 @@
import MetaTrader5 as mt5
import pandas as pd
from logger import logger
from core.data.abc import DataProvider
class LiveDataProvider(DataProvider):
"""实盘数据提供者 — 封装真实MT5 API调用"""
@property
def is_live(self):
return True
def initialize(self):
if not mt5.initialize():
logger.error("MT5初始化失败")
return False
logger.info("MT5连接成功")
return True
def shutdown(self):
mt5.shutdown()
logger.info("MT5连接已关闭")
def get_current_price(self, symbol):
tick = mt5.symbol_info_tick(symbol)
if tick:
last_price = tick.last if tick.last != 0 else (tick.bid + tick.ask) / 2
return {
'bid': tick.bid, 'ask': tick.ask, 'last': last_price,
'time': pd.to_datetime(tick.time, unit='s')
}
logger.error(f"无法获取 {symbol} 的价格数据")
return None
def get_historical_data(self, symbol, timeframe, count, **kwargs):
return mt5.copy_rates_from_pos(symbol, timeframe, 0, count)
def get_account_info(self):
return mt5.account_info()
def get_positions(self, symbol):
return mt5.positions_get(symbol=symbol)
def get_symbol_info(self, symbol):
return mt5.symbol_info(symbol)
def send_order(self, symbol, order_type, volume):
price_data = self.get_current_price(symbol)
if not price_data:
logger.error(f"无法获取 {symbol} 价格,无法下单")
return None
price = price_data['ask'] if order_type == "buy" else price_data['bid']
if not mt5.terminal_info().trade_allowed:
logger.error("MT5终端未启用自动交易")
return None
account_info = mt5.account_info()
if account_info and not account_info.trade_allowed:
logger.error("当前账户不允许自动交易")
return None
symbol_info = mt5.symbol_info(symbol)
if not symbol_info:
logger.error(f"无法获取 {symbol} 的品种信息")
return None
# MT5 filling_mode 是位图,用位与判断
fm = symbol_info.filling_mode
# MQL5 filling_mode 位图: FOK=1, IOC=2
if fm & 1:
filling_mode = mt5.ORDER_FILLING_FOK
elif fm & 2:
filling_mode = mt5.ORDER_FILLING_IOC
else:
filling_mode = mt5.ORDER_FILLING_RETURN
order_type_mt5 = mt5.ORDER_TYPE_BUY if order_type == "buy" else mt5.ORDER_TYPE_SELL
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": volume,
"type": order_type_mt5,
"price": price,
"deviation": 20,
"magic": 234000,
"comment": f"{order_type} order",
"type_filling": filling_mode,
}
result = mt5.order_send(request)
if result and hasattr(result, 'retcode'):
if result.retcode == 10027:
logger.error("自动交易被禁用")
elif result.retcode == 10030:
if filling_mode != mt5.ORDER_FILLING_IOC:
request["type_filling"] = mt5.ORDER_FILLING_IOC
result = mt5.order_send(request)
if result and result.retcode == 10030 and filling_mode != mt5.ORDER_FILLING_FOK:
request["type_filling"] = mt5.ORDER_FILLING_FOK
result = mt5.order_send(request)
elif result.retcode != 10009:
logger.error(f"下单失败,错误代码: {result.retcode}")
return result
def close_position(self, ticket, symbol, volume):
positions = self.get_positions(symbol)
if not positions:
logger.error("未找到任何持仓")
return False
target_position = None
for pos in positions:
if pos.ticket == ticket:
target_position = pos
break
if not target_position:
logger.error(f"未找到ticket为 {ticket} 的持仓")
return False
tick = mt5.symbol_info_tick(symbol)
if not tick:
logger.error(f"无法获取 {symbol} 的当前价格")
return False
if target_position.type == mt5.POSITION_TYPE_BUY:
close_price = tick.bid
order_type = mt5.ORDER_TYPE_SELL
else:
close_price = tick.ask
order_type = mt5.ORDER_TYPE_BUY
symbol_info = mt5.symbol_info(symbol)
if not symbol_info:
return False
fm = symbol_info.filling_mode
# MQL5 filling_mode 位图: FOK=1, IOC=2
if fm & 1:
filling_mode = mt5.ORDER_FILLING_FOK
elif fm & 2:
filling_mode = mt5.ORDER_FILLING_IOC
else:
filling_mode = mt5.ORDER_FILLING_IOC
request = {
"action": mt5.TRADE_ACTION_DEAL,
"position": target_position.ticket,
"symbol": symbol,
"volume": volume,
"type": order_type,
"price": close_price,
"deviation": 20,
"magic": 234000,
"comment": f"Close position {ticket}",
"type_filling": filling_mode,
}
result = mt5.order_send(request)
if result is None:
logger.error(f"平仓请求返回None: Ticket={ticket}")
return False
if result.retcode == mt5.TRADE_RETCODE_DONE:
logger.info(f"平仓成功: Ticket {ticket}")
return True
else:
logger.error(f"平仓失败: Ticket={ticket}, 错误码={result.retcode}")
return False
+154
View File
@@ -0,0 +1,154 @@
import numpy as np
import pandas as pd
from utils.constants import TIMEFRAME_TO_MINUTES, RESAMPLE_RULES, PERIOD_M1
class MultiTimeframeDataStore:
"""多周期数据存储 — 从M1数据按需生成更高周期的OHLC数据
回测/优化器模式:加载M1数据后,通过 pandas resample 生成 H1/H4/D1 等。
实盘模式:不需要此组件(DataProvider 直接返回 MT5 原生数据)。
用法:
store = MultiTimeframeDataStore()
store.load_m1_data(m1_rates_array)
h1_data = store.get_historical_data("XAUUSD", PERIOD_H1, 100, current_index=5000)
"""
def __init__(self):
self._main_df = None
self._main_timeframe = PERIOD_M1
self._cache = {} # timeframe: DataFrame
def load_m1_data(self, rates: np.ndarray) -> None:
"""加载M1原始数据并初始化缓存"""
df = pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s')
df.set_index('time', inplace=True)
df.sort_index(inplace=True)
self._main_df = df
self._cache = {PERIOD_M1: df}
@property
def main_df(self) -> pd.DataFrame:
return self._main_df
@property
def length(self) -> int:
if self._main_df is None:
return 0
return len(self._main_df)
def _resample(self, timeframe: int) -> pd.DataFrame:
"""从M1数据 resample 到目标周期"""
if timeframe == PERIOD_M1:
return self._main_df
rule = RESAMPLE_RULES.get(timeframe)
if rule is None:
raise ValueError(f"不支持的周期: {timeframe}")
# 确保 M1 DataFrame 包含 OHLCV 列
m1 = self._main_df[['open', 'high', 'low', 'close']].copy()
if 'tick_volume' in self._main_df.columns:
m1['tick_volume'] = self._main_df['tick_volume']
resampled = m1.resample(rule, label='right', closed='right').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
})
if 'tick_volume' in m1.columns:
resampled['tick_volume'] = m1['tick_volume'].resample(
rule, label='right', closed='right').sum()
# 生成 time 列(Unix秒)
resampled['time'] = [int(ts.timestamp()) for ts in resampled.index]
# 丢掉由未来数据填充出来的最后一个未完成 bar
last_m1_time = self._main_df.index[-1]
last_complete = resampled[resampled.index <= last_m1_time]
if len(last_complete) > 0:
resampled = last_complete
elif len(resampled) > 0:
logger_warning = None
try:
from logger import logger
logger_warning = logger
except Exception:
pass
if logger_warning:
logger_warning.warning(
f"resample {rule}: 所有bar都在最后M1时间之后,返回空DataFrame"
)
resampled = resampled.iloc[0:0]
return resampled
def ensure_timeframe(self, timeframe: int) -> pd.DataFrame:
"""确保指定周期的数据已缓存,若未缓存则从M1生成"""
if timeframe in self._cache:
return self._cache[timeframe]
if self._main_df is None:
raise RuntimeError("必须先调用 load_m1_data()")
tf_df = self._resample(timeframe)
self._cache[timeframe] = tf_df
return tf_df
def _find_tf_index(self, timeframe: int, m1_bar_index: int) -> int:
"""将M1 bar索引映射到目标周期的已完成bar索引
返回目标周期中已经完成(不会看到未来)的最后一个 bar 的索引。
即:目标周期 bar 的结束时间 <= 当前M1 bar 的时间。
"""
if self._main_df is None:
return -1
tf_df = self.ensure_timeframe(timeframe)
current_time = self._main_df.index[m1_bar_index]
# 找到结束时间 <= current_time 的最后一个目标周期 bar
positions = tf_df.index.get_indexer([current_time], method='bfill')
tf_idx = positions[0]
# bfill 返回的 bar 可能结束时间 > current_time(未完成),需要退回一个
if tf_idx >= 0 and tf_idx < len(tf_df):
if tf_df.index[tf_idx] > current_time:
tf_idx -= 1
return tf_idx
def get_historical_data(self, symbol: str, timeframe: int,
count: int, current_index: int) -> np.ndarray | None:
"""从 current_index 位置向前获取 count 条指定周期的历史数据
Args:
symbol: 品种名(当前未使用,为接口一致性保留)
timeframe: MT5周期常量
count: 需要的bar数量
current_index: 当前M1 bar的索引(0-based
Returns:
numpy recarray 或 None(数据不足时)
"""
if self._main_df is None or current_index >= self.length:
return None
tf_idx = self._find_tf_index(timeframe, current_index)
if tf_idx < count - 1:
return None
tf_df = self.ensure_timeframe(timeframe)
start = tf_idx - count + 1
end = tf_idx + 1
# 转换为MT5兼容的记录数组格式
# 避免 index name 与 'time' 列冲突
records = tf_df.iloc[start:end].reset_index(drop=True).to_records(index=False)
return np.array(records)
+119
View File
@@ -0,0 +1,119 @@
import pandas as pd
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from core.data.abc import DataProvider
class AttrDict(dict):
"""支持属性访问的 dict,兼容 isinstance(x, dict) 检查"""
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(key)
class RemoteDataProvider(DataProvider):
"""远端数据提供者 — 通过 HTTP 调用 MT5 代理服务"""
def __init__(self, host="127.0.0.1", port=5555):
self.base_url = f"http://{host}:{port}/api"
self._session = requests.Session()
retry = Retry(total=3, backoff_factor=0.5, status_forcelist=[502, 503, 504])
self._session.mount("http://", HTTPAdapter(max_retries=retry))
@property
def is_live(self):
return True
def initialize(self):
try:
resp = self._session.post(f"{self.base_url}/initialize", timeout=10)
return resp.json().get("success", False)
except Exception:
return False
def shutdown(self):
try:
self._session.post(f"{self.base_url}/shutdown", timeout=5)
except Exception:
pass
def get_current_price(self, symbol):
try:
resp = self._session.get(f"{self.base_url}/price/{symbol}", timeout=10)
if resp.status_code != 200:
return None
data = resp.json()
data['time'] = pd.to_datetime(data['time'], unit='s')
return data
except Exception:
return None
def get_historical_data(self, symbol, timeframe, count, **kwargs):
try:
resp = self._session.get(
f"{self.base_url}/historical/{symbol}/{timeframe}/{count}",
timeout=30,
)
if resp.status_code != 200:
return None
return resp.json()["rates"]
except Exception:
return None
def get_account_info(self):
try:
resp = self._session.get(f"{self.base_url}/account", timeout=10)
if resp.status_code != 200:
return None
return AttrDict(resp.json())
except Exception:
return None
def get_positions(self, symbol):
try:
resp = self._session.get(f"{self.base_url}/positions/{symbol}", timeout=10)
if resp.status_code != 200:
return None
return [AttrDict(p) for p in resp.json()["positions"]]
except Exception:
return None
def get_symbol_info(self, symbol):
try:
resp = self._session.get(f"{self.base_url}/symbol/{symbol}", timeout=10)
if resp.status_code != 200:
return None
return AttrDict(resp.json())
except Exception:
return None
def send_order(self, symbol, order_type, volume):
try:
resp = self._session.post(
f"{self.base_url}/order",
json={"symbol": symbol, "order_type": order_type, "volume": volume},
timeout=10,
)
if resp.status_code != 200:
return None
return AttrDict(resp.json())
except Exception:
return None
def close_position(self, ticket, symbol, volume):
try:
resp = self._session.post(
f"{self.base_url}/close",
json={"ticket": ticket, "symbol": symbol, "volume": volume},
timeout=10,
)
if resp.status_code != 200:
return False
return resp.json().get("success", False)
except Exception:
return False
+46
View File
@@ -0,0 +1,46 @@
import numpy as np
def _to_native(val):
"""numpy 类型转 Python 原生类型"""
if isinstance(val, (np.integer,)):
return int(val)
if isinstance(val, (np.floating,)):
return float(val)
if isinstance(val, np.ndarray):
return val.tolist()
return val
def _mt5_to_dict(obj):
"""将 MT5 对象转为 dict,兼容 _asdict() / _fields / __dict__"""
if hasattr(obj, '_asdict'):
return {k: _to_native(v) for k, v in obj._asdict().items()}
if hasattr(obj, '_fields'):
return {f: _to_native(getattr(obj, f)) for f in obj._fields}
if hasattr(obj, '__dict__'):
return {k: _to_native(v) for k, v in obj.__dict__.items()}
return {}
def serialize_account_info(info):
return _mt5_to_dict(info) if info else None
def serialize_position(pos):
return _mt5_to_dict(pos) if pos else None
def serialize_symbol_info(info):
return _mt5_to_dict(info) if info else None
def serialize_order_result(result):
return _mt5_to_dict(result) if result else None
def serialize_rates(rates):
if rates is None:
return None
names = rates.dtype.names
return [{name: _to_native(row[name]) for name in names} for row in rates]
-373
View File
@@ -1,373 +0,0 @@
import MetaTrader5 as mt5
import pandas as pd
import random
from logger import logger
from abc import ABC, abstractmethod
from collections import namedtuple
from config import SIMULATION_CONFIG
class DataProvider(ABC):
"""数据提供者抽象基类"""
@property
def is_live(self):
return False
@abstractmethod
def initialize(self):
pass
@abstractmethod
def shutdown(self):
pass
@abstractmethod
def get_current_price(self, symbol):
pass
@abstractmethod
def get_historical_data(self, symbol, timeframe, count, **kwargs):
pass
@abstractmethod
def get_account_info(self):
pass
@abstractmethod
def get_positions(self, symbol):
pass
@abstractmethod
def get_symbol_info(self, symbol):
pass
@abstractmethod
def send_order(self, symbol, order_type, volume):
pass
@abstractmethod
def close_position(self, ticket, symbol, volume):
pass
class LiveDataProvider(DataProvider):
"""实盘数据提供者"""
@property
def is_live(self):
return True
def initialize(self):
if not mt5.initialize():
logger.error("MT5初始化失败")
return False
logger.info("MT5连接成功")
return True
def shutdown(self):
mt5.shutdown()
logger.info("MT5连接已关闭")
def get_current_price(self, symbol):
tick = mt5.symbol_info_tick(symbol)
if tick:
# 对于某些品种(如XAUUSD),last价格可能为0,使用bid/ask的平均值作为替代
last_price = tick.last if tick.last != 0 else (tick.bid + tick.ask) / 2
logger.info(f"获取价格数据 - {symbol}: bid={tick.bid}, ask={tick.ask}, last={tick.last}, 使用last_price={last_price}")
return {'bid': tick.bid, 'ask': tick.ask, 'last': last_price, 'time': pd.to_datetime(tick.time, unit='s')}
else:
logger.error(f"无法获取 {symbol} 的价格数据")
return None
def get_historical_data(self, symbol, timeframe, count, **kwargs):
return mt5.copy_rates_from_pos(symbol, timeframe, 0, count)
def get_account_info(self):
return mt5.account_info()
def get_positions(self, symbol):
return mt5.positions_get(symbol=symbol)
def get_symbol_info(self, symbol):
return mt5.symbol_info(symbol)
def send_order(self, symbol, order_type, volume):
price_data = self.get_current_price(symbol)
if not price_data:
logger.error(f"无法获取 {symbol} 价格,无法下单")
return None
# 对于买入使用ask价格,卖出使用bid价格
price = price_data['ask'] if order_type == "buy" else price_data['bid']
logger.info(f"下单价格 - {symbol} {order_type}: 使用价格 {price}")
# 检查终端是否允许自动交易
if not mt5.terminal_info().trade_allowed:
logger.error("MT5终端未启用自动交易!请在MT5中点击'自动交易'按钮或按Ctrl+E启用")
return None
# 检查账户是否允许交易
account_info = mt5.account_info()
if account_info and not account_info.trade_allowed:
logger.error("当前账户不允许自动交易,请联系 broker")
return None
# 获取品种信息以确定支持的填充模式
symbol_info = mt5.symbol_info(symbol)
if not symbol_info:
logger.error(f"无法获取 {symbol} 的品种信息")
return None
# 确定填充模式:优先使用品种支持的填充模式
filling_mode = mt5.ORDER_FILLING_IOC # 默认使用IOC
if symbol_info.filling_mode == 1: # 只支持FILLING模式
filling_mode = mt5.ORDER_FILLING_FOK
elif symbol_info.filling_mode == 2: # 只支持RETURN模式
filling_mode = mt5.ORDER_FILLING_RETURN
elif symbol_info.filling_mode == 3: # 支持所有模式
filling_mode = mt5.ORDER_FILLING_RETURN # 优先使用RETURN
logger.info(f"使用填充模式: {filling_mode} (品种支持模式: {symbol_info.filling_mode})")
order_type_mt5 = mt5.ORDER_TYPE_BUY if order_type == "buy" else mt5.ORDER_TYPE_SELL
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": volume,
"type": order_type_mt5,
"price": price,
"deviation": 20,
"magic": 234000,
"comment": f"{order_type} order",
"type_filling": filling_mode,
}
result = mt5.order_send(request)
# 检查常见错误代码
if result and hasattr(result, 'retcode'):
if result.retcode == 10027:
logger.error("自动交易被禁用!请在MT5中:")
logger.error("1. 点击工具栏的'自动交易'按钮(绿色播放图标)")
logger.error("2. 或按快捷键 Ctrl+E")
logger.error("3. 确保按钮变为绿色状态")
elif result.retcode == 10030:
logger.error("订单填充模式不支持!尝试其他填充模式...")
# 如果RETURN模式失败,尝试IOC模式
if filling_mode != mt5.ORDER_FILLING_IOC:
logger.info("尝试使用IOC填充模式...")
request["type_filling"] = mt5.ORDER_FILLING_IOC
result = mt5.order_send(request)
# 如果IOC模式也失败,尝试FOK模式
if result and result.retcode == 10030 and filling_mode != mt5.ORDER_FILLING_FOK:
logger.info("尝试使用FOK填充模式...")
request["type_filling"] = mt5.ORDER_FILLING_FOK
result = mt5.order_send(request)
elif result.retcode != 10009: # 10009 = 成功
logger.error(f"下单失败,错误代码: {result.retcode}, 错误信息: {result.comment if hasattr(result, 'comment') else '未知错误'}")
return result
def close_position(self, ticket, symbol, volume):
positions = self.get_positions(symbol)
if not positions:
logger.error(f"未找到任何持仓")
return False
target_position = None
for pos in positions:
if pos.ticket == ticket:
target_position = pos
break
if not target_position:
logger.error(f"未找到ticket为 {ticket} 的持仓")
return False
# 获取当前tick价格
tick = mt5.symbol_info_tick(symbol)
if not tick:
logger.error(f"无法获取 {symbol} 的当前价格")
return False
# 根据持仓类型确定平仓价格和订单类型
if target_position.type == mt5.POSITION_TYPE_BUY: # 多单平仓
close_price = tick.bid
order_type = mt5.ORDER_TYPE_SELL
else: # 空单平仓
close_price = tick.ask
order_type = mt5.ORDER_TYPE_BUY
# 获取品种信息以确定支持的填充模式
symbol_info = mt5.symbol_info(symbol)
if not symbol_info:
logger.error(f"无法获取 {symbol} 的品种信息")
return False
# 确定填充模式:优先使用IOC(即时成交或取消)
filling_mode = mt5.ORDER_FILLING_IOC
if symbol_info.filling_mode == 1: # 只支持FILLING模式
filling_mode = mt5.ORDER_FILLING_FOK
elif symbol_info.filling_mode == 2: # 只支持RETURN模式
filling_mode = mt5.ORDER_FILLING_RETURN
elif symbol_info.filling_mode == 3: # 支持所有模式
filling_mode = mt5.ORDER_FILLING_IOC # 优先使用IOC
request = {
"action": mt5.TRADE_ACTION_DEAL,
"position": target_position.ticket,
"symbol": symbol,
"volume": volume,
"type": order_type,
"price": close_price,
"deviation": 20,
"magic": 234000,
"comment": f"Close position {ticket}",
"type_filling": filling_mode,
}
logger.info(f"发送平仓请求: Ticket={ticket}, 价格={close_price}, 类型={order_type}, 填充模式={filling_mode}")
result = mt5.order_send(request)
if result.retcode == mt5.TRADE_RETCODE_DONE:
logger.info(f"平仓成功: Ticket {ticket}")
return True
else:
logger.error(f"平仓失败: Ticket={ticket}, 错误码={result.retcode}, 错误信息={result.comment}")
# 记录常见错误的具体原因
if result.retcode == 10027:
logger.error("自动交易被禁用!请在MT5中启用自动交易")
elif result.retcode == 10006:
logger.error("请求被拒绝,可能是价格变动过快")
elif result.retcode == 10013:
logger.error("无效请求,检查参数是否正确")
elif result.retcode == 10016:
logger.error("无效的成交量,检查手数是否符合要求")
return False
class DryRunDataProvider(DataProvider):
"""
纸上交易提供者 (Paper Trading / Dry Run)
- 使用来自MT5的实时价格数据
- 模拟下单和持仓,不发送真实订单
- 考虑点差影响
"""
def __init__(self, initial_equity=10000, leverage=100):
self.simulated_ticket_counter = 0
self.equity = initial_equity
self.leverage = leverage
# 使用LiveDataProvider作为获取市场数据的来源
self._live_data_source = LiveDataProvider()
# 从配置获取点差
self.spread = SIMULATION_CONFIG.get("spread", 16)
def initialize(self):
logger.info("纸上交易模式初始化...")
return self._live_data_source.initialize()
def shutdown(self):
logger.info("纸上交易模式关闭.")
self._live_data_source.shutdown()
# --- 数据获取方法 (委托给LiveDataProvider) ---
def get_current_price(self, symbol):
price_data = self._live_data_source.get_current_price(symbol)
if price_data:
# 计算双向点差值(各一半)
spread_half = self.spread * 0.01 / 2 # XAUUSD: 1点 = 0.01,双向点差各一半
# 使用中间价计算双向点差
mid_price = price_data['last']
# 返回考虑双向点差的价格
return {
'bid': mid_price - spread_half, # 卖出价格(中间价 - 点差/2
'ask': mid_price + spread_half, # 买入价格(中间价 + 点差/2
'last': mid_price, # 最后成交价(中间价)
'time': price_data['time']
}
return None
def get_historical_data(self, symbol, timeframe, count, **kwargs):
return self._live_data_source.get_historical_data(symbol, timeframe, count, **kwargs)
def get_symbol_info(self, symbol):
return self._live_data_source.get_symbol_info(symbol)
# --- 模拟状态和交易方法 ---
def get_account_info(self):
# 账户信息是模拟的,因为没有真实账户活动
return {'equity': self.equity}
def get_positions(self, symbol):
# 持仓信息是模拟的,由PositionManager在内部管理,这里返回空列表
return []
def send_order(self, symbol, order_type, volume):
self.simulated_ticket_counter += 1
price_data = self.get_current_price(symbol)
price = price_data['last'] if price_data else "N/A"
logger.info(f"[纸上交易] 模拟下单: {order_type} {volume:.2f}{symbol} @ {price}")
return {'order': self.simulated_ticket_counter}
def close_position(self, ticket, symbol, volume):
price_data = self.get_current_price(symbol)
price = price_data['last'] if price_data else "N/A"
logger.info(f"[纸上交易] 模拟平仓: Ticket {ticket} @ {price}")
return True
class BacktestDataProvider(DataProvider):
"""回测数据提供者"""
def __init__(self, df, initial_equity=10000, leverage=100):
self.df = df
self.current_index = 0
self.equity = initial_equity
self.leverage = leverage
self.simulated_ticket_counter = 0
# 从配置获取点差
self.spread = SIMULATION_CONFIG.get("spread", 16)
def initialize(self): return True
def shutdown(self): pass
def get_account_info(self):
return {'equity': self.equity}
def get_positions(self, symbol): return []
def get_symbol_info(self, symbol):
return {
'trade_contract_size': SIMULATION_CONFIG['contract_size'],
'volume_step': SIMULATION_CONFIG['volume_step'],
'volume_min': SIMULATION_CONFIG['volume_min'],
'volume_max': SIMULATION_CONFIG['volume_max'],
}
def get_current_price(self, symbol):
if self.current_index >= len(self.df):
return None
row = self.df.iloc[self.current_index]
# 计算双向点差值(各一半)
spread_half = self.spread * 0.01 / 2 # XAUUSD: 1点 = 0.01,双向点差各一半
# 返回考虑双向点差的价格
return {
'bid': row.close - spread_half, # 卖出价格(中间价 - 点差/2
'ask': row.close + spread_half, # 买入价格(中间价 + 点差/2
'last': row.close, # 最后成交价(中间价)
'time': row.name
}
def get_historical_data(self, symbol, timeframe, count, **kwargs):
if self.current_index < count:
return None
end_index = self.current_index + 1
start_index = max(0, end_index - count)
return self.df.iloc[start_index:end_index].to_dict('records')
def send_order(self, symbol, order_type, volume):
self.simulated_ticket_counter += 1
logger.info(f"[回测模式] 下单: {order_type} {volume:.2f}{symbol}")
return {'order': self.simulated_ticket_counter}
def close_position(self, ticket, symbol, volume):
logger.info(f"[回测模式] 平仓: Ticket {ticket}")
return True
def tick(self):
if self.current_index < len(self.df) - 1:
self.current_index += 1
return True
return False
+3 -32
View File
@@ -1,32 +1,3 @@
from .market_state import MarketStateAnalyzer
from .position_manager import PositionManager
class RiskController:
"""
风险管理控制器 (已重构为依赖注入)
"""
def __init__(self, data_provider, trade_direction="both"):
self.data_provider = data_provider
self.position_manager = PositionManager(data_provider, trade_direction)
self.market_state_analyzer = MarketStateAnalyzer(data_provider)
def process_trading_signal(self, direction, current_price, signal_strength=0.0):
return self.position_manager.open_position(direction, current_price, signal_strength)
def monitor_positions(self, current_price, dry_run=False):
self.position_manager.monitor_positions(current_price, dry_run)
def sync_state(self):
"""从数据源同步权益和持仓"""
self.position_manager.update_equity()
self.position_manager.sync_positions()
def get_account_status(self):
return self.position_manager.get_account_status()
def get_positions(self):
return self.position_manager.get_positions()
def save_trade_history(self, base_filename):
self.position_manager.save_trade_history(base_filename)
from core.risk.market_state import MarketStateAnalyzer
from core.risk.position import PositionManager
from core.risk.controller import RiskController
+37
View File
@@ -0,0 +1,37 @@
from core.risk.market_state import MarketStateAnalyzer
from core.risk.position import PositionManager
class RiskController:
"""风险管理控制器 — 门面模式,组合 PositionManager 和 MarketStateAnalyzer"""
def __init__(self, data_provider, trade_direction="both",
risk_config: dict = None,
market_state_analyzer: MarketStateAnalyzer = None):
self.data_provider = data_provider
self.position_manager = PositionManager(data_provider, trade_direction, risk_config)
self.market_state_analyzer = market_state_analyzer or MarketStateAnalyzer(data_provider)
self.trade_direction = trade_direction
def process_trading_signal(self, direction, current_price, signal_strength=0.0):
return self.position_manager.open_position(direction, current_price, signal_strength)
def monitor_positions(self, current_price, dry_run=False):
self.position_manager.monitor_positions(current_price, dry_run)
def sync_state(self):
self.position_manager.update_equity()
self.position_manager.sync_positions()
def get_account_status(self):
return {
'equity': self.position_manager.total_equity,
'open_positions': len(self.position_manager.positions),
'trade_summary': self.position_manager.get_trade_summary(),
}
def get_positions(self):
return self.position_manager.positions
def save_trade_history(self, base_filename):
self.position_manager.save_trade_history(base_filename)
+108
View File
@@ -0,0 +1,108 @@
"""退出规则 — 策略模式,每个规则负责判断是否需要平仓"""
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ExitContext:
"""退出规则所需的上下文"""
current_profit_pct: float
peak_profit_pct: float
entry_time: datetime
current_time: datetime
symbol: str = ""
position: dict = None
class BaseExitRule:
"""退出规则基类"""
def __init__(self, config: dict):
self.config = config
def check(self, ctx: ExitContext) -> tuple[str, str]:
"""返回 (action, reason)action 为 "close""none" """
return "none", ""
@property
def name(self) -> str:
return self.__class__.__name__
class StopLossRule(BaseExitRule):
"""固定止损"""
def check(self, ctx: ExitContext) -> tuple[str, str]:
if ctx.current_profit_pct <= self.config.get("stop_loss_pct", -0.10):
return "close", "止损触发"
return "none", ""
class TakeProfitRule(BaseExitRule):
"""固定止盈"""
def check(self, ctx: ExitContext) -> tuple[str, str]:
if ctx.current_profit_pct >= self.config.get("take_profit_pct", 0.20):
return "close", "止盈触发"
return "none", ""
class TrailingStopRule(BaseExitRule):
"""追踪止损"""
def check(self, ctx: ExitContext) -> tuple[str, str]:
min_profit = self.config.get("min_profit_for_trailing", 0.01)
retracement_pct = self.config.get("profit_retracement_pct", 0.10)
if ctx.peak_profit_pct <= min_profit:
return "none", ""
stop_level = ctx.peak_profit_pct * (1 - retracement_pct)
if ctx.current_profit_pct <= stop_level:
return "close", (
f"追踪止损触发 "
f"(峰值 {ctx.peak_profit_pct:.2%} 回落至 {ctx.current_profit_pct:.2%})"
)
return "none", ""
class TimeExitRule(BaseExitRule):
"""时间退出"""
def check(self, ctx: ExitContext) -> tuple[str, str]:
if not self.config.get("enable_time_based_exit", False):
return "none", ""
max_minutes = self.config.get("max_holding_minutes", 60)
min_profit = self.config.get("min_profit_for_time_exit", 0.001)
holding_seconds = (ctx.current_time - ctx.entry_time).total_seconds()
holding_minutes = holding_seconds / 60
if holding_minutes > max_minutes and ctx.current_profit_pct < min_profit:
return "close", f"超时平仓 (持仓超过{max_minutes}分钟且盈利未达标)"
return "none", ""
class ExitRuleEngine:
"""退出规则引擎 — 按顺序执行所有注册的规则,返回第一个触发的退出
注意:每日亏损上限检查在 PositionManager._check_max_daily_loss 中处理(开仓时),
不在退出规则中重复,避免两套计算逻辑不一致。
"""
def __init__(self, config: dict):
self.rules = [
StopLossRule(config),
TakeProfitRule(config),
TrailingStopRule(config),
TimeExitRule(config),
]
def check(self, ctx: ExitContext) -> tuple[str, str]:
for rule in self.rules:
action, reason = rule.check(ctx)
if action == "close":
return action, reason
return "none", ""
+235 -81
View File
@@ -1,46 +1,75 @@
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 config import (
MARKET_STATE_CONFIG, SYMBOL, DEFAULT_WEIGHTS,
TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS,
MARKET_STATE_WEIGHTS, CONFIDENCE_THRESHOLDS
)
from utils.constants import PERIOD_H1
from core.data.multi_tf import MultiTimeframeDataStore
class MarketStateAnalyzer:
"""市场状态分析器(重写版)
两种工作模式:
- 实盘模式:get_market_state() 实时从 DataProvider 获取H1数据计算
- 回测模式:precompute_states() 一次性预计算全序列,get_market_state(bar_index) 从缓存读取
关键改进:
1. timeframe 可配置,不再硬编码
2. precompute_states() 使用 MultiTimeframeDataStore 获取正确周期的数据
3. get_strategy_weights() 接受 individual_weights 参数使优化器权重基因真正生效
"""
市场状态分析器 (支持参数优化)
"""
def __init__(self, data_provider, market_state_params=None, trend_weights=None, trend_thresholds=None, confidence_thresholds=None):
def __init__(self, data_provider=None,
timeframe: int = PERIOD_H1,
market_state_params: dict = None,
trend_weights: dict = None,
trend_thresholds: dict = None,
confidence_thresholds: dict = None):
self.data_provider = data_provider
self.symbol = SYMBOL
self.timeframe = 16385 # 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)
self.timeframe = timeframe
# 参数合并
ms_config = market_state_params or MARKET_STATE_CONFIG
self.trend_period = ms_config.get("trend_period", 50)
self.retracement_tolerance = ms_config.get("retracement_tolerance", 0.30)
self.volume_period = ms_config.get("volume_period", 20)
self.volume_ma_period = ms_config.get("volume_ma_period", 10)
self.hourly_data_count = ms_config.get("hourly_data_count", 100)
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.current_trend = "none"
self.trend_peak = 0.0
self.trend_trough = float('inf')
# 回测模式缓存
self._precomputed_states: list | None = None
# ── 指标计算(从现有逻辑提取)──
def _calculate_volume_indicators(self, df):
df['volume'] = df['tick_volume']
df['volume'] = df.get('tick_volume', df.get('volume', pd.Series(0, index=df.index)))
df['volume_ma'] = df['volume'].rolling(self.volume_ma_period).mean()
df['volume_ratio'] = df['volume'] / df['volume_ma']
volume_corr = df['close'].rolling(self.volume_period).corr(df['volume'])
return df, volume_corr.iloc[-1] if len(volume_corr) > 0 and not pd.isna(volume_corr.iloc[-1]) else 0
last_corr = volume_corr.iloc[-1] if len(volume_corr) > 0 and not pd.isna(volume_corr.iloc[-1]) else 0
return df, last_corr
def _calculate_momentum_indicators(self, df):
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.ewm(com=13, min_periods=14).mean()
avg_loss = loss.ewm(com=13, min_periods=14).mean()
rs = avg_gain / avg_loss.replace(0, float('nan')).fillna(0)
df['rsi'] = 100 - (100 / (1 + rs))
exp1 = df['close'].ewm(span=12).mean()
exp2 = df['close'].ewm(span=26).mean()
@@ -50,51 +79,100 @@ class MarketStateAnalyzer:
df['ma20'] = df['close'].rolling(20).mean()
df['ma50'] = df['close'].rolling(50).mean()
return df
def _calculate_price_breakout_score(self, df):
current_price = df['close'].iloc[-1]
high_period = df['high'].rolling(self.trend_period).max().iloc[-2]
low_period = df['low'].rolling(self.trend_period).min().iloc[-2]
lookback = min(self.trend_period, len(df) - 1)
if lookback < 1:
return 0.5
high_period = df['high'].rolling(self.trend_period).max().iloc[-2] if len(df) >= 2 else df['high'].iloc[-1]
low_period = df['low'].rolling(self.trend_period).min().iloc[-2] if len(df) >= 2 else df['low'].iloc[-1]
price_range = high_period - low_period
if price_range == 0: return 0.5
if price_range == 0:
return 0.5
price_position = (current_price - low_period) / price_range
if current_price > high_period: return 0.8
elif current_price < low_period: return 0.2
else: return np.clip(price_position, 0, 1)
if current_price > high_period:
return 0.8
elif current_price < low_period:
return 0.2
return float(np.clip(price_position, 0, 1))
def _calculate_volume_score(self, df, volume_corr):
current_volume = df['volume'].iloc[-1]
volume_ma = df['volume_ma'].iloc[-1]
volume_ratio = current_volume / volume_ma if volume_ma > 0 else 1
volume_ratio = current_volume / volume_ma if (not pd.isna(volume_ma) and volume_ma > 0) else 1
score = 0.5
if volume_ratio > self.thresholds['volume_spike']: score += 0.3
if volume_corr > 0.5: score += 0.2
elif volume_corr < -0.5: score -= 0.2
return np.clip(score, 0, 1)
if volume_ratio > self.thresholds.get('volume_spike', 1.5):
score += 0.3
if volume_corr > 0.5:
score += 0.2
elif volume_corr < -0.5:
score -= 0.2
return float(np.clip(score, 0, 1))
def _calculate_momentum_score(self, df):
current_rsi = df['rsi'].iloc[-1] if not pd.isna(df['rsi'].iloc[-1]) else 50
current_macd_hist = df['macd_histogram'].iloc[-1] if not pd.isna(df['macd_histogram'].iloc[-1]) else 0
rsi_val = df['rsi'].iloc[-1] if not pd.isna(df['rsi'].iloc[-1]) else 50
macd_hist = df['macd_histogram'].iloc[-1] if not pd.isna(df['macd_histogram'].iloc[-1]) else 0
score = 0.5
if current_rsi > self.thresholds['overbought']: score += 0.2
elif current_rsi < self.thresholds['oversold']: score -= 0.2
if current_macd_hist > 0: score += 0.2
else: score -= 0.2
return np.clip(score, 0, 1)
if rsi_val > self.thresholds.get('overbought', 70):
score += 0.2
elif rsi_val < self.thresholds.get('oversold', 30):
score -= 0.2
if macd_hist > 0:
score += 0.2
else:
score -= 0.2
return float(np.clip(score, 0, 1))
def _calculate_ma_score(self, df):
current_price = df['close'].iloc[-1]
ma20 = df['ma20'].iloc[-1] if not pd.isna(df['ma20'].iloc[-1]) else current_price
ma50 = df['ma50'].iloc[-1] if not pd.isna(df['ma50'].iloc[-1]) else current_price
score = 0.5
if current_price > ma20 > ma50: score += 0.3
elif current_price < ma20 < ma50: score -= 0.3
if current_price > ma20 > ma50:
score += 0.3
elif current_price < ma20 < ma50:
score -= 0.3
if len(df) >= 3:
ma20_slope = df['ma20'].iloc[-1] - df['ma20'].iloc[-3]
ma50_slope = df['ma50'].iloc[-1] - df['ma50'].iloc[-3]
if ma20_slope > 0 and ma50_slope > 0: score += 0.2
elif ma20_slope < 0 and ma50_slope < 0: score -= 0.2
return np.clip(score, 0, 1)
if ma20_slope > 0 and ma50_slope > 0:
score += 0.2
elif ma20_slope < 0 and ma50_slope < 0:
score -= 0.2
return float(np.clip(score, 0, 1))
def _calculate_state_from_df(self, df):
"""从 DataFrame 计算市场状态 — 提取自原 get_market_state() 的 DataFrame 处理部分"""
df = df.copy()
df, volume_corr = self._calculate_volume_indicators(df)
df = self._calculate_momentum_indicators(df)
price_score = self._calculate_price_breakout_score(df)
volume_score = self._calculate_volume_score(df, volume_corr)
momentum_score = self._calculate_momentum_score(df)
ma_score = self._calculate_ma_score(df)
trend_strength = (
price_score * self.indicator_weights.get('price_breakout', 0.35) +
volume_score * self.indicator_weights.get('volume_confirmation', 0.25) +
momentum_score * self.indicator_weights.get('momentum oscillator', 0.20) +
ma_score * self.indicator_weights.get('moving_average', 0.20)
)
strong = self.thresholds.get('strong_trend', 0.6)
weak = self.thresholds.get('weak_trend', 0.3)
if trend_strength >= strong:
state, confidence = "uptrend", min(0.95, trend_strength)
elif trend_strength <= (1 - strong):
state, confidence = "downtrend", min(0.95, 1 - trend_strength)
elif trend_strength >= weak:
state, confidence = "ranging", 0.6
else:
state, confidence = "none", 0.4
return state, confidence
def _update_trend_state(self, df, new_state):
current_price = df['close'].iloc[-1]
@@ -110,42 +188,118 @@ class MarketStateAnalyzer:
elif self.current_trend == "downtrend" and current_price > self.trend_trough * (1 + self.retracement_tolerance):
self.current_trend = "none"
def get_market_state(self):
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.hourly_data_count)
# ── 预计算(回测/优化器模式)──
def precompute_from_multitf(self, multi_tf: MultiTimeframeDataStore) -> None:
"""从 MultiTimeframeDataStore 预计算全序列市场状态
这是回测和优化器模式的核心入口。调用后,get_market_state(bar_index)
直接从缓存读取,不再需要实时计算。
Args:
multi_tf: 已加载M1数据的 MultiTimeframeDataStore 实例
"""
h1_df = multi_tf.ensure_timeframe(self.timeframe)
if h1_df is None or len(h1_df) == 0:
logger.warning("H1数据为空,所有市场状态设为 'none'")
self._precomputed_states = [("none", 0.0)] * multi_tf.length
return
m1_index = multi_tf.main_df.index
h1_index = h1_df.index
m1_length = len(m1_index)
# 计算每个H1 bar的状态
h1_states = []
for i in range(len(h1_df)):
lookback = self.trend_period + 10
start = max(0, i - lookback)
df_slice = h1_df.iloc[start:i + 1]
state, conf = self._calculate_state_from_df(df_slice)
h1_states.append((state, conf))
# 映射到M1时间轴
self._precomputed_states = []
h1_pos = 0
for m1_time in m1_index:
while h1_pos < len(h1_index) - 1 and m1_time >= h1_index[h1_pos + 1]:
h1_pos += 1
if h1_pos < len(h1_states):
self._precomputed_states.append(h1_states[h1_pos])
else:
self._precomputed_states.append(("none", 0.0))
# 前几个H1 bar之前的M1 bar标记为 "none"
first_h1_time = h1_index[0] if len(h1_index) > 0 else None
if first_h1_time is not None:
for idx, m1_time in enumerate(m1_index):
if m1_time < first_h1_time:
self._precomputed_states[idx] = ("none", 0.0)
logger.info(f"市场状态预计算完成: {len(h1_df)} 个H1 bar → {len(self._precomputed_states)} 个M1 bar")
# ── 状态获取 ──
def get_market_state(self, bar_index: int = None) -> tuple:
"""获取市场状态
回测模式(有预计算缓存):从缓存读取第 bar_index 个状态
实盘模式(无缓存):通过 DataProvider 实时计算
"""
if self._precomputed_states is not None:
idx = bar_index if bar_index is not None else 0
if idx >= len(self._precomputed_states):
return "none", 0.0
return self._precomputed_states[idx]
# 实盘模式
if self.data_provider is None:
return "none", 0.0
rates = self.data_provider.get_historical_data(
self.symbol, self.timeframe, self.hourly_data_count
)
if rates is None or len(rates) < max(self.trend_period, 50):
return "none", 0.0
df = pd.DataFrame(rates)
df, volume_corr = self._calculate_volume_indicators(df)
df = self._calculate_momentum_indicators(df)
price_score = self._calculate_price_breakout_score(df)
volume_score = self._calculate_volume_score(df, volume_corr)
momentum_score = self._calculate_momentum_score(df)
ma_score = self._calculate_ma_score(df)
trend_strength = (price_score * self.indicator_weights['price_breakout'] +
volume_score * self.indicator_weights['volume_confirmation'] +
momentum_score * self.indicator_weights['momentum oscillator'] +
ma_score * self.indicator_weights['moving_average'])
if trend_strength >= self.thresholds['strong_trend']: state, confidence = "uptrend", min(0.95, trend_strength)
elif trend_strength <= (1 - self.thresholds['strong_trend']): state, confidence = "downtrend", min(0.95, 1 - trend_strength)
elif trend_strength >= self.thresholds['weak_trend']: state, confidence = "ranging", 0.6
else: state, confidence = "none", 0.4
state, confidence = self._calculate_state_from_df(df)
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()}
# ── 权重计算 ──
def get_strategy_weights(self, market_state: str, confidence: float,
individual_weights: dict = None) -> dict:
"""获取策略权重
关键改进:individual_weights 参数使优化器的权重基因真正生效。
Args:
market_state: "uptrend" / "downtrend" / "ranging" / "none"
confidence: 置信度 0~1
individual_weights: 优化器传入 {config_key: weight},非None时优先使用
Returns:
{config_key: weight} 用于信号加权组合
"""
# ★ 优化器模式:使用基因中的权重
if individual_weights is not None:
base_weights = dict(individual_weights)
else:
return DEFAULT_WEIGHTS
# 正常模式:从配置获取市场状态对应权重
base_weights = dict(MARKET_STATE_WEIGHTS.get(market_state, DEFAULT_WEIGHTS))
high_conf = self.confidence_thresholds.get("high_confidence", 0.7)
medium_conf = self.confidence_thresholds.get("medium_confidence", 0.4)
if confidence > high_conf:
return {k: v * confidence for k, v in base_weights.items()}
elif confidence > medium_conf:
return {
k: (v * confidence + DEFAULT_WEIGHTS.get(k, 1.0) * (1 - confidence))
for k, v in base_weights.items()
}
else:
# 低置信度:individual_weights 优先(优化器模式),否则回退到 DEFAULT_WEIGHTS
return dict(individual_weights) if individual_weights is not None else dict(DEFAULT_WEIGHTS)
+405
View File
@@ -0,0 +1,405 @@
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, SIMULATION_CONFIG
)
from core.risk.exit_rules import ExitRuleEngine, ExitContext
class PositionManager:
"""持仓管理器(重写版)
关键改进:
1. update_equity() 包含浮盈浮亏
2. max_daily_loss 真正落地生效
3. 退出规则委托给 ExitRuleEngine(策略模式)
"""
def __init__(self, data_provider, trade_direction="both", risk_config: dict = None,
persist_peaks: bool = True):
self.data_provider = data_provider
self.symbol = SYMBOL
self.trade_direction = trade_direction
self._persist_peaks = persist_peaks
# 风险管理参数
self._risk_config = risk_config or RISK_CONFIG
risk = self._risk_config
self.stop_loss_pct = risk.get("stop_loss_pct", -0.10)
self.profit_retracement_pct = risk.get("profit_retracement_pct", 0.10)
self.min_profit_for_trailing = risk.get("min_profit_for_trailing", 0.01)
self.take_profit_pct = risk.get("take_profit_pct", 0.20)
self.max_holding_minutes = risk.get("max_holding_minutes", 60)
self.min_profit_for_time_exit = risk.get("min_profit_for_time_exit", 0.001)
self.enable_time_based_exit = RISK_CONFIG_CONST.get("enable_time_based_exit", False)
self.max_daily_loss = risk.get("max_daily_loss", -0.30)
# 资金管理
self.initial_capital = INITIAL_CAPITAL
self.long_capital_pct = CAPITAL_ALLOCATION.get("long_pct", 0.5)
self.short_capital_pct = CAPITAL_ALLOCATION.get("short_pct", 0.5)
# 持仓和交易记录
self.positions = []
self.closed_trades = []
self.total_equity = self.initial_capital
# 冷却期:平仓后N根K线内不重新开仓(防反复进出)
self.cooldown_bars = risk.get("cooldown_bars", 30)
self._cooldown_counter = 0
# 退出规则引擎
exit_config = {
"stop_loss_pct": self.stop_loss_pct,
"take_profit_pct": self.take_profit_pct,
"min_profit_for_trailing": self.min_profit_for_trailing,
"profit_retracement_pct": self.profit_retracement_pct,
"enable_time_based_exit": self.enable_time_based_exit,
"max_holding_minutes": self.max_holding_minutes,
"min_profit_for_time_exit": self.min_profit_for_time_exit,
"max_daily_loss": self.max_daily_loss,
}
self.exit_engine = ExitRuleEngine(exit_config)
# 峰值数据持久化(优化器中禁用文件I/O避免多进程竞争)
self.peak_data_file = "position_peaks.json"
if self._persist_peaks:
self._load_peak_data()
# ── 仓位计算 ──
def _calculate_position_size(self, capital_to_allocate, current_price):
price = current_price['last']
if not isinstance(price, (int, float)) or price == 0:
logger.error(f"价格无效: {price}")
return 0.0
symbol_info = self.data_provider.get_symbol_info(self.symbol)
if not symbol_info:
logger.error(f"无法获取 {self.symbol} 的合约信息")
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
value_of_one_lot = price * contract_size
if value_of_one_lot == 0:
return 0.0
volume = capital_to_allocate / value_of_one_lot
volume = round(volume / volume_step) * volume_step
volume = max(min_volume, min(volume, max_volume))
return volume
# ── 开仓 ──
def open_position(self, direction, current_price, signal_strength=0.0, dry_run=False):
# 冷却期检查
if self._cooldown_counter > 0:
self._cooldown_counter -= 1
return False
position_type = 'long' if direction == 'buy' else 'short'
# 开仓前更新权益,确保仓位大小基于最新资产(含浮盈浮亏)
self.update_equity()
if dry_run:
execution_price = current_price['ask'] if direction == 'buy' else current_price['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
# ★ 每日亏损检查(幽灵代码落地)
current_time = current_price.get('time', pd.Timestamp.now())
if self._check_max_daily_loss(current_time):
logger.warning("当日亏损已达上限,禁止开新仓")
return False
# 最大持仓数检查 — 优先使用 risk_config 传入值,回退到 REALTIME_CONFIG
max_key = f'max_{position_type}_positions'
max_positions = self._risk_config.get(max_key, None)
if max_positions is None:
from config import REALTIME_CONFIG
max_positions = REALTIME_CONFIG.get(max_key, 1)
current_count = len([p for p in self.positions if p['position_type'] == position_type])
if 0 < max_positions <= current_count:
logger.info(f"已达到最大{position_type}持仓数 ({max_positions}),忽略信号")
return False
# 资金分配
capital_pct = self.long_capital_pct if direction == 'buy' else self.short_capital_pct
capital_for_trade = self.total_equity * capital_pct
position_volume = self._calculate_position_size(capital_for_trade, {'last': execution_price})
if position_volume <= 0:
logger.info("仓位大小为0,无法开仓")
return False
order_result = self.data_provider.send_order(self.symbol, direction, position_volume)
if order_result is None:
logger.error("订单返回None")
return False
try:
order_id = order_result['order'] if isinstance(order_result, dict) else order_result.order
except Exception as e:
logger.error(f"解析订单ID失败: {e}")
return False
if order_result and order_id > 0:
new_position = {
'ticket': order_id,
'symbol': self.symbol,
'entry_price': execution_price,
'entry_time': current_time,
'position_type': position_type,
'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}")
return True
else:
logger.error(f"开仓失败: {direction} @ {execution_price:.2f}")
return False
# ── 持仓监控 ──
def monitor_positions(self, current_price, dry_run=False):
if not self.positions:
return
current_time = current_price.get('time', pd.Timestamp.now())
positions_to_remove = []
for position in self.positions:
# 确定平仓执行价格
if dry_run:
close_price = current_price['bid'] if position['position_type'] == 'long' else current_price['ask']
else:
close_price = current_price['last']
# 用执行价格计算盈亏(与平仓时一致,避免中间价偏差)
pnl_pct = self._calculate_pnl_pct(position, close_price)
old_peak = position.get('peak_profit_pct', 0)
new_peak = max(old_peak, pnl_pct)
position['peak_profit_pct'] = new_peak
if new_peak > old_peak:
self._save_peak_data()
# 退出规则检查
ctx = ExitContext(
current_profit_pct=pnl_pct,
peak_profit_pct=new_peak,
entry_time=position['entry_time'],
current_time=current_time,
symbol=self.symbol,
position=position,
)
action, reason = self.exit_engine.check(ctx)
if action == "close":
logger.info(f"平仓信号触发 (Ticket: {position['ticket']}): {reason}")
success = self.data_provider.close_position(
position['ticket'], position['symbol'], position['quantity']
)
if success:
self._record_closed_trade(position, close_price, reason)
positions_to_remove.append(position)
else:
logger.error(f"平仓失败, Ticket: {position['ticket']}")
if positions_to_remove:
self.positions = [p for p in self.positions if p not in positions_to_remove]
self._cooldown_counter = self.cooldown_bars
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
def _calculate_unrealized_pnl(self) -> float:
"""★ 新增:计算所有持仓的浮动盈亏"""
if not self.positions:
return 0.0
current_price_data = self.data_provider.get_current_price(self.symbol)
if not current_price_data:
return 0.0
current_price = current_price_data['last']
symbol_info = self.data_provider.get_symbol_info(self.symbol)
contract_size = (symbol_info['trade_contract_size']
if symbol_info and isinstance(symbol_info, dict)
else 100) if symbol_info else 100
total = 0.0
for pos in self.positions:
if pos['position_type'] == 'long':
pnl = (current_price - pos['entry_price']) * pos['quantity'] * contract_size
else:
pnl = (pos['entry_price'] - current_price) * pos['quantity'] * contract_size
total += pnl
return total
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 symbol_info and isinstance(symbol_info, dict)
else 100) if symbol_info else 100
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
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
})
self.closed_trades.append(trade_record)
logger.info(f"平仓记录 #{position['ticket']}: 盈亏: ${pnl:.2f}")
# ── 权益管理 ──
def update_equity(self):
"""★ 修正:包含浮盈浮亏"""
if self.data_provider.is_live:
account_info = self.data_provider.get_account_info()
if account_info:
equity = account_info if isinstance(account_info, dict) else account_info.equity
if isinstance(equity, dict):
equity = equity.get('equity', self.total_equity)
self.total_equity = equity
else:
realized_pnl = sum(t['profit_loss'] for t in self.closed_trades)
unrealized_pnl = self._calculate_unrealized_pnl()
self.total_equity = self.initial_capital + realized_pnl + unrealized_pnl
def _check_max_daily_loss(self, current_time) -> bool:
"""★ 新增:检查当日最大亏损是否触发"""
if self.max_daily_loss >= 0:
return False
today = current_time.date() if hasattr(current_time, 'date') else pd.Timestamp(current_time).date()
daily_pnl = sum(
t['profit_loss'] for t in self.closed_trades
if hasattr(t.get('close_time'), 'date') and t['close_time'].date() == today
or hasattr(t.get('entry_time'), 'date') and t['entry_time'].date() == today
)
daily_loss_pct = daily_pnl / self.initial_capital if self.initial_capital > 0 else 0
return daily_loss_pct <= self.max_daily_loss
# ── 同步 ──
def sync_positions(self):
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()
existing_peaks = {pos['ticket']: pos.get('peak_profit_pct', 0.0) for pos in self.positions}
merged_peaks = {**existing_peaks, **saved_peaks}
self.positions.clear()
for pos in live_positions:
restored_peak = merged_peaks.get(pos.ticket, 0.0)
new_position = {
'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',
'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)}")
self.update_equity()
# ── 交易摘要 ──
def get_trade_summary(self) -> dict:
if not self.closed_trades:
return {
'total_trades': 0, 'winning_trades': 0, 'losing_trades': 0,
'win_rate': 0, 'total_profit_loss': 0.0,
'avg_profit_loss': 0.0, 'max_profit': 0.0, 'max_loss': 0.0
}
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': float(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")
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:
return json.load(f)
except Exception as e:
logger.error(f"加载峰值数据失败: {e}")
return {}
def _save_peak_data(self):
try:
if not self._persist_peaks:
return
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)
except Exception as e:
logger.error(f"保存峰值数据失败: {e}")
def cleanup_peak_data(self):
try:
if not self._persist_peaks:
return
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)
current_tickets = {pos['ticket'] for pos in self.positions}
cleaned = {t: p for t, p in peak_data.items() if t in current_tickets}
with open(self.peak_data_file, 'w', encoding='utf-8') as f:
json.dump(cleaned, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"清理峰值数据失败: {e}")
-429
View File
@@ -1,429 +0,0 @@
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
class PositionManager:
"""
持仓管理器 - 统一管理所有持仓操作和交易记录 (已重构为依赖注入)
"""
def __init__(self, data_provider, trade_direction="both"):
self.data_provider = data_provider
self.symbol = SYMBOL
self.trade_direction = trade_direction
# 风险管理参数
self.stop_loss_pct = RISK_CONFIG.get("stop_loss_pct", -0.10)
self.profit_retracement_pct = RISK_CONFIG.get("profit_retracement_pct", 0.10)
self.min_profit_for_trailing = RISK_CONFIG.get("min_profit_for_trailing", 0.0003)
self.take_profit_pct = RISK_CONFIG.get("take_profit_pct", 0.20)
self.enable_time_based_exit = RISK_CONFIG_CONST.get("enable_time_based_exit", False)
self.max_holding_minutes = RISK_CONFIG.get("max_holding_minutes", 60)
self.min_profit_for_time_exit = RISK_CONFIG.get("min_profit_for_time_exit", 0.0005)
# 资金管理参数
self.initial_capital = INITIAL_CAPITAL
self.long_capital_pct = CAPITAL_ALLOCATION.get("long_pct", 0.5)
self.short_capital_pct = CAPITAL_ALLOCATION.get("short_pct", 0.5)
self.max_daily_loss = RISK_CONFIG.get("max_daily_loss", -0.10)
# 持仓和交易记录
self.positions = []
self.closed_trades = []
self.total_equity = self.initial_capital
# 持久化文件路径
self.peak_data_file = "position_peaks.json"
# 初始化时加载峰值数据
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
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
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
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
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
# 从配置中获取最大持仓限制
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
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}")
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
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
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,
}
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
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 action == "close":
logger.info(f"平仓信号触发 (Ticket: {position['ticket']}): {reason}")
# 在dry_run模式下,计算考虑点差的平仓价格
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:
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
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
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
})
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 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 "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
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
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 = {
'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',
'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
}
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")
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("峰值数据文件不存在,使用空数据")
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}")
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}")
View File
+61
View File
@@ -0,0 +1,61 @@
import pandas as pd
class SignalCombiner:
"""信号组合器 — 统一加权求和和阈值过滤逻辑
消除 start_backtest.py / optimizer.py / backtest_engine.py 中的重复。
"""
@staticmethod
def combine_vectorized(signals_df: pd.DataFrame,
weights: dict,
buy_threshold: float,
sell_threshold: float) -> pd.Series:
"""向量化信号组合 — 用于回测预生成阶段
Args:
signals_df: 每列是一个策略的回测信号序列,列名为策略类名
weights: {config_key 或 class_name: 权重}
buy_threshold: 加权和大于此值 → 买入信号(1)
sell_threshold: 加权和小于此值 → 卖出信号(-1)
Returns:
最终信号序列 (-1/0/1)
"""
combined = pd.Series(0.0, index=signals_df.index)
for col in signals_df.columns:
# 权重匹配:先尝试类名,再尝试config_key
w = weights.get(col, 1.0)
combined += signals_df[col] * w
final = pd.Series(0, index=combined.index)
final[combined > buy_threshold] = 1
final[combined < sell_threshold] = -1
return final
@staticmethod
def combine_at_bar(signals: dict,
weights: dict,
buy_threshold: float,
sell_threshold: float) -> int:
"""单 bar 信号组合 — 用于回测/优化器逐 bar 循环
Args:
signals: {策略标识: signal_value (-1/0/1 或 0.0~1.0)}
weights: {策略标识: 权重}
buy_threshold: 加权和买入阈值
sell_threshold: 加权和卖出阈值
Returns:
-1/0/1
"""
weighted_sum = sum(
signals.get(name, 0) * weights.get(name, 1.0)
for name in set(signals) | set(weights)
)
if weighted_sum > buy_threshold:
return 1
elif weighted_sum < sell_threshold:
return -1
return 0
+70
View File
@@ -0,0 +1,70 @@
"""策略注册表 — 单例,消除代码中多处的策略列表重复
用法:
registry = StrategyRegistry()
registry.register('ma_cross', MACrossStrategy)
registry.instantiate_all(SYMBOL, TIMEFRAME) # 或传入参数字典
config_key = registry.class_name_to_config_key('MACrossStrategy')
"""
from config import STRATEGY_CONFIG
class StrategyRegistry:
"""策略注册表单例 — 所有策略信息的唯一数据源"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._strategies = {} # config_key → StrategyClass
cls._instance._instances = {} # config_key → instance
return cls._instance
def register(self, config_key: str, strategy_class: type) -> None:
self._strategies[config_key] = strategy_class
def instantiate_all(self, symbol: str, timeframe: int,
params_dict: dict = None,
data_provider=None) -> dict:
"""实例化所有已注册策略
Args:
data_provider: 回测传Nonerun_backtest不需要),实盘传DataProvider实例
Returns:
{config_key: strategy_instance}
"""
if params_dict is None:
params_dict = STRATEGY_CONFIG
instances = {}
for key, cls in self._strategies.items():
params = params_dict.get(key, {})
instances[key] = cls(data_provider, symbol, timeframe, **params)
self._instances = instances
return instances
@property
def instances(self) -> dict:
return self._instances
@property
def all_strategies(self) -> dict:
"""返回 {config_key: StrategyClass}"""
return dict(self._strategies)
def class_name_to_config_key(self, class_name: str) -> str | None:
for key, cls in self._strategies.items():
if cls.__name__ == class_name:
return key
return None
def config_key_to_class_name(self, config_key: str) -> str:
return self._strategies[config_key].__name__
def get_instance(self, config_key: str):
return self._instances.get(config_key)
def __contains__(self, config_key: str) -> bool:
return config_key in self._strategies
View File
View File
+192
View File
@@ -0,0 +1,192 @@
"""统一回测引擎 — 消除 start_backtest.py 和 optimizer.py 中的回测循环重复"""
import sys
import os
import pandas as pd
import numpy as np
from tqdm import tqdm
from logger import logger
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.data import MultiTimeframeDataStore, BacktestDataProvider
from core.risk.controller import RiskController
from core.risk.market_state import MarketStateAnalyzer
from core.signal.registry import StrategyRegistry
from core.signal.combiner import SignalCombiner
from execution.weights import DynamicWeightManager
from config import (
SYMBOL, TIMEFRAME, INITIAL_CAPITAL, SIGNAL_THRESHOLDS, DEFAULT_WEIGHTS,
BACKTEST_CONFIG, RISK_CONFIG,
MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS
)
from utils.constants import PERIOD_H1
class BacktestEngine:
"""统一回测引擎
支持两种模式
- standard: 使用 DEFAULT_WEIGHTS 的固定权重回测
- dynamic: 使用 MarketStateAnalyzer 的动态权重回测
"""
def __init__(self, symbol: str = SYMBOL, timeframe: int = TIMEFRAME,
initial_capital: float = INITIAL_CAPITAL,
use_dynamic_weights: bool = False):
self.symbol = symbol
self.timeframe = timeframe
self.initial_capital = initial_capital
self.use_dynamic_weights = use_dynamic_weights
# 延迟初始化
self.multi_tf = None
self.data_provider = None
self.risk_controller = None
self.registry = StrategyRegistry()
def load_data(self, rates: np.ndarray) -> None:
"""加载M1数据并初始化组件"""
if rates is None or len(rates) == 0:
raise ValueError("无法加载历史数据")
self.multi_tf = MultiTimeframeDataStore()
self.multi_tf.load_m1_data(rates)
self.data_provider = BacktestDataProvider(self.multi_tf, self.initial_capital)
# 预计算市场状态(动态权重模式)
analyzer = None
if self.use_dynamic_weights:
analyzer = MarketStateAnalyzer(
timeframe=PERIOD_H1,
market_state_params=MARKET_STATE_CONFIG,
trend_weights=TREND_INDICATOR_WEIGHTS,
trend_thresholds=TREND_THRESHOLDS,
confidence_thresholds=CONFIDENCE_THRESHOLDS,
)
analyzer.precompute_from_multitf(self.multi_tf)
self.risk_controller = RiskController(
self.data_provider,
trade_direction=BACKTEST_CONFIG.get('trade_direction', 'both'),
risk_config=RISK_CONFIG,
market_state_analyzer=analyzer,
)
self.weight_manager = DynamicWeightManager(
self.data_provider,
market_state_analyzer=analyzer or MarketStateAnalyzer(),
)
def _instantiate_strategies(self, params_dict: dict = None) -> dict:
"""使用给定参数实例化所有策略"""
return self.registry.instantiate_all(self.symbol, self.timeframe, params_dict)
def precompute_signals(self, strategies: dict) -> pd.DataFrame:
"""预生成所有策略信号"""
logger.info("开始预生成所有策略信号...")
signals_df = pd.DataFrame(index=self.multi_tf.main_df.index)
for config_key, strategy in tqdm(strategies.items(), desc="生成策略信号"):
try:
sig = strategy.run_backtest(self.multi_tf.main_df)
if sig is not None and len(sig) > 0:
signals_df[config_key] = sig
logger.debug(f"策略 {config_key} 信号生成成功")
else:
logger.warning(f"策略 {config_key} 返回空信号")
signals_df[config_key] = pd.Series(0, index=signals_df.index)
except Exception as e:
logger.error(f"策略 {config_key} 执行失败: {e}")
signals_df[config_key] = pd.Series(0, index=signals_df.index)
logger.info(f"信号预生成完成: {len(signals_df.columns)} 个策略")
return signals_df
def run(self, rates: np.ndarray,
strategy_params: dict = None,
weights: dict = None,
buy_threshold: float = None,
sell_threshold: float = None) -> dict:
"""执行完整回测
Args:
rates: MT5原始M1数据numpy数组
strategy_params: {config_key: {param_name: value}} 策略参数覆写
weights: {config_key: weight} 策略权重None则使用 DEFAULT_WEIGHTS
buy_threshold: 买入阈值
sell_threshold: 卖出阈值
Returns:
dict: 交易摘要
"""
self.load_data(rates)
buy_th = buy_threshold or SIGNAL_THRESHOLDS.get('buy_threshold', 1.5)
sell_th = sell_threshold or SIGNAL_THRESHOLDS.get('sell_threshold', -1.5)
# 实例化策略并预计算信号
strategies = self._instantiate_strategies(strategy_params)
signals_df = self.precompute_signals(strategies)
# 信号组合
if weights is not None:
use_weights = weights
else:
use_weights = DEFAULT_WEIGHTS
combined_signals = SignalCombiner.combine_vectorized(
signals_df, use_weights, buy_th, sell_th
)
logger.info("回测主循环开始...")
total_bars = len(self.multi_tf.main_df)
for bar_index in tqdm(range(total_bars), desc="回测执行"):
try:
current_price = self.data_provider.get_current_price(self.symbol)
if not current_price:
self.data_provider.tick()
continue
# 动态权重模式:每bar重新计算权重并重新组合信号
if self.use_dynamic_weights and self.weight_manager.analyzer._precomputed_states is not None:
bar_weights = self.weight_manager.get_weights_for_bar(bar_index)
bar_signals = {col: signals_df[col].iloc[bar_index] for col in signals_df.columns}
current_signal = SignalCombiner.combine_at_bar(
bar_signals, bar_weights, buy_th, sell_th
)
else:
current_signal = combined_signals.iloc[bar_index]
if current_signal == 1:
self.risk_controller.process_trading_signal("buy", current_price, 1.0)
elif current_signal == -1:
self.risk_controller.process_trading_signal("sell", current_price, 1.0)
self.risk_controller.monitor_positions(current_price, dry_run=True)
self.data_provider.tick()
except Exception as e:
logger.error(f"回测循环在索引 {bar_index} 出错: {e}")
self.data_provider.tick()
continue
# 生成报告
summary = self.risk_controller.position_manager.get_trade_summary()
self._print_report(summary)
# 保存交易记录
try:
self.risk_controller.save_trade_history("backtest_trades")
except Exception as e:
logger.error(f"保存交易记录失败: {e}")
return summary
def _print_report(self, summary: dict) -> None:
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)
-95
View File
@@ -1,95 +0,0 @@
import pandas as pd
from tqdm import tqdm
from core.risk import RiskController
from config import SIGNAL_THRESHOLDS, BACKTEST_CONFIG, SPREAD
from logger import logger
class BacktestEngine:
def __init__(self, df, trade_direction="both"):
self.df = df
self.trade_direction = trade_direction
self.spread = BACKTEST_CONFIG.get("spread", SPREAD)
def run_strategy(self, strategy):
signals = strategy.run_backtest(self.df)
buy_count = (signals == 1).sum()
sell_count = (signals == -1).sum()
logger.info(f"策略 {strategy.__class__.__module__} 信号统计 - 买入: {buy_count}, 卖出: {sell_count}")
return signals
def combine_signals(self, signals_list, weights):
df_signals = pd.concat(signals_list, axis=1).fillna(0)
weighted_signals = df_signals * weights
combined = weighted_signals.sum(axis=1)
def apply_threshold(score):
if score > SIGNAL_THRESHOLDS["buy_threshold"]:
return 1
elif score < SIGNAL_THRESHOLDS["sell_threshold"]:
return -1
else:
return 0
combined_signal = combined.apply(apply_threshold)
buy_signals = (combined_signal == 1).sum()
sell_signals = (combined_signal == -1).sum()
logger.info(f"组合信号统计 - 买入: {buy_signals}, 卖出: {sell_signals}")
return combined_signal
def run_backtest(self, signals, symbol="XAUUSD"):
df = self.df.copy()
df['signal'] = signals.shift(1).fillna(0)
risk_controller = RiskController(self.trade_direction)
logger.info(f"回测开始: 交易方向={self.trade_direction}")
# 使用tqdm创建进度条
for i in tqdm(range(1, len(df)), desc=f"Backtesting ({len(df)} bars)"):
current_signal = df['signal'].iloc[i]
# 计算考虑双向点差的买卖价格
close_price = df['close'].iloc[i]
spread_points = self.spread
spread_half = spread_points * 0.01 / 2 # XAUUSD: 1点 = 0.01,双向点差各一半
current_price = {
'bid': close_price - spread_half, # 卖出价格(中间价 - 点差/2
'ask': close_price + spread_half, # 买入价格(中间价 + 点差/2
'last': close_price # 最后成交价(中间价)
}
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), dry_run=True
)
risk_controller.monitor_positions(current_price, dry_run=True)
risk_controller.position_manager.force_close_all_positions(df['close'].iloc[-1], dry_run=True)
logger.info("回测完成,生成性能报告...")
summary = risk_controller.position_manager.get_trade_summary()
logger.info("=" * 80)
logger.info("回测性能报告")
logger.info("=" * 80)
logger.info(f" 总交易次数: {summary['total_trades']}")
logger.info(f" 盈利次数: {summary['winning_trades']}")
logger.info(f" 亏损次数: {summary['losing_trades']}")
logger.info(f" 胜率: {summary['win_rate']:.2f}%")
logger.info("-" * 40)
logger.info(f" 总盈亏: ${summary['total_profit_loss']:.2f}")
logger.info(f" 平均每笔交易盈亏: ${summary['avg_profit_loss']:.2f}")
logger.info(f" 最大盈利: ${summary['max_profit']:.2f}")
logger.info(f" 最大亏损: ${summary['max_loss']:.2f}")
logger.info("=" * 80)
risk_controller.position_manager.save_to_csv("backtest_trades.csv")
risk_controller.position_manager.save_to_json("backtest_trades.json")
return summary
-67
View File
@@ -1,67 +0,0 @@
from strategies import ma_cross, rsi, bollinger, mean_reversion, momentum_breakout, macd, kdj, turtle, daily_breakout, wave_theory
from config import DEFAULT_WEIGHTS, SYMBOL, TIMEFRAME
from logger import logger
from core.risk.market_state import MarketStateAnalyzer
class DynamicWeightManager:
"""
动态权重管理器
"""
def __init__(self, data_provider, market_state_params=None, trend_weights=None, trend_thresholds=None, confidence_thresholds=None):
self.data_provider = data_provider
self.market_state_analyzer = MarketStateAnalyzer(
data_provider,
market_state_params=market_state_params,
trend_weights=trend_weights,
trend_thresholds=trend_thresholds,
confidence_thresholds=confidence_thresholds
)
# 策略类和它们的初始化参数的映射
self.strategy_blueprints = {
'ma_cross': (ma_cross.MACrossStrategy, {}),
'rsi': (rsi.RSIStrategy, {}),
'bollinger': (bollinger.BollingerStrategy, {}),
'mean_reversion': (mean_reversion.MeanReversionStrategy, {}),
'momentum_breakout': (momentum_breakout.MomentumBreakoutStrategy, {}),
'macd': (macd.MACDStrategy, {}),
'kdj': (kdj.KDJStrategy, {}),
'turtle': (turtle.TurtleStrategy, {}),
'daily_breakout': (daily_breakout.DailyBreakoutStrategy, {}),
'wave_theory': (wave_theory.WaveTheoryStrategy, {})
}
self.strategy_instances = self._create_strategy_instances()
self.current_weights = None
self.current_market_state = "none"
self.current_confidence = 0.0
def _create_strategy_instances(self):
instances = {}
for name, (strategy_class, params) in self.strategy_blueprints.items():
instances[name] = strategy_class(self.data_provider, SYMBOL, TIMEFRAME, **params)
return instances
def get_current_strategies_and_weights(self):
market_state, confidence = self.market_state_analyzer.get_market_state()
dynamic_weights = self.market_state_analyzer.get_strategy_weights(market_state, confidence)
self.current_weights = dynamic_weights
self.current_market_state = market_state
self.current_confidence = confidence
strategies_with_weights = []
for name, weight in dynamic_weights.items():
if name in self.strategy_instances:
strategies_with_weights.append((self.strategy_instances[name], weight))
return strategies_with_weights
def get_weight_info(self):
return {
'market_state': self.current_market_state,
'confidence': self.current_confidence,
'weights': self.current_weights
}
+578
View File
@@ -0,0 +1,578 @@
"""遗传算法优化器(重写版)
关键改进
1. 权重基因通过 individual_weights MarketStateAnalyzer.get_strategy_weights() 真正生效
2. 使用 StrategyRegistry 单例消除重复的策略列表
3. 使用 MultiTimeframeDataStore 支持正确的多周期市场状态分析
4. evaluate_fitness bar 调用 get_market_state(bar_index) 获取每根bar的动态权重
"""
import random
import numpy as np
from deap import base, creator, tools, algorithms
import multiprocessing
import pandas as pd
import sys
import os
from datetime import datetime
from tqdm import tqdm
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.data import MultiTimeframeDataStore, BacktestDataProvider
from core.risk.market_state import MarketStateAnalyzer
from core.risk.position import PositionManager
from core.signal.registry import StrategyRegistry
from core.signal.combiner import SignalCombiner
from config import (
SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE,
USE_DATE_RANGE, INITIAL_CAPITAL, SIGNAL_THRESHOLDS, DEFAULT_WEIGHTS,
RISK_CONFIG, GENETIC_OPTIMIZER_CONFIG,
MARKET_STATE_CONFIG, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS, CONFIDENCE_THRESHOLDS
)
from utils.constants import PERIOD_H1
from core.utils import get_rates, initialize, shutdown
from logger import logger
# 导入策略模块确保 StrategyRegistry 已注册
import strategies # noqa: F401
# ── 全局缓存 ──
_multi_tf: MultiTimeframeDataStore | None = None
_cached_signals: pd.DataFrame | None = None
_registry: StrategyRegistry | None = None
def init_worker():
"""多进程worker初始化:抑制日志噪音"""
import logging
logging.getLogger().setLevel(logging.WARNING)
for name in ["StrategyLogger", "PositionManager", "RiskController", "DataProvider"]:
logging.getLogger(name).setLevel(logging.WARNING)
# ── 参数定义(与旧版一致,保持兼容)──
PARAMETER_DEFINITIONS = [
# MACrossStrategy
{'name': 'ma_cross_short_window', 'type': 'int', 'min': 3, 'max': 15, 'strategy': 'MACrossStrategy'},
{'name': 'ma_cross_long_window', 'type': 'int', 'min': 10, 'max': 60, 'strategy': 'MACrossStrategy'},
# RSIStrategy
{'name': 'rsi_period', 'type': 'int', 'min': 7, 'max': 21, 'strategy': 'RSIStrategy'},
{'name': 'rsi_overbought', 'type': 'int', 'min': 65, 'max': 80, 'strategy': 'RSIStrategy'},
{'name': 'rsi_oversold', 'type': 'int', 'min': 20, 'max': 35, 'strategy': 'RSIStrategy'},
# BollingerStrategy
{'name': 'bollinger_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'BollingerStrategy'},
{'name': 'bollinger_std_dev', 'type': 'float', 'min': 1.5, 'max': 3.0, 'strategy': 'BollingerStrategy'},
# MACDStrategy
{'name': 'macd_fast_ema', 'type': 'int', 'min': 8, 'max': 20, 'strategy': 'MACDStrategy'},
{'name': 'macd_slow_ema', 'type': 'int', 'min': 20, 'max': 35, 'strategy': 'MACDStrategy'},
{'name': 'macd_signal_period', 'type': 'int', 'min': 5, 'max': 15, 'strategy': 'MACDStrategy'},
# MeanReversionStrategy
{'name': 'mean_reversion_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MeanReversionStrategy'},
{'name': 'mean_reversion_std_dev', 'type': 'float', 'min': 1.5, 'max': 3.0, 'strategy': 'MeanReversionStrategy'},
# MomentumBreakoutStrategy
{'name': 'momentum_breakout_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MomentumBreakoutStrategy'},
{'name': 'momentum_breakout_momentum_period', 'type': 'int', 'min': 5, 'max': 30, 'strategy': 'MomentumBreakoutStrategy'},
# KDJStrategy
{'name': 'kdj_period', 'type': 'int', 'min': 5, 'max': 21, 'strategy': 'KDJStrategy'},
# TurtleStrategy
{'name': 'turtle_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'TurtleStrategy'},
# WaveTheoryStrategy
{'name': 'wave_ema_short', 'type': 'int', 'min': 3, 'max': 10, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_ema_medium', 'type': 'int', 'min': 8, 'max': 20, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_ema_long', 'type': 'int', 'min': 20, 'max': 50, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_period', 'type': 'int', 'min': 10, 'max': 40, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_range_period', 'type': 'int', 'min': 10, 'max': 40, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_adx_period', 'type': 'int', 'min': 10, 'max': 25, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_momentum_period', 'type': 'int', 'min': 7, 'max': 21, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_range_threshold', 'type': 'float', 'min': 0.002, 'max': 0.01, 'strategy': 'WaveTheoryStrategy'},
{'name': 'wave_adx_threshold', 'type': 'int', 'min': 15, 'max': 35, 'strategy': 'WaveTheoryStrategy'},
# DailyBreakoutStrategy
{'name': 'daily_breakout_bars_count', 'type': 'int', 'min': 720, 'max': 2880, 'strategy': 'DailyBreakoutStrategy'},
# Signal Thresholds
{'name': 'buy_threshold', 'type': 'float', 'min': 0.5, 'max': 3.0, 'strategy': 'signal'},
{'name': 'sell_threshold', 'type': 'float', 'min': -3.0, 'max': -0.5, 'strategy': 'signal'},
# Risk Management
{'name': 'stop_loss_pct', 'type': 'float', 'min': -0.05, 'max': -0.01, 'strategy': 'risk'},
{'name': 'profit_retracement_pct', 'type': 'float', 'min': 0.05, 'max': 0.20, 'strategy': 'risk'},
{'name': 'min_profit_for_trailing', 'type': 'float', 'min': 0.005, 'max': 0.02, 'strategy': 'risk'},
{'name': 'take_profit_pct', 'type': 'float', 'min': 0.10, 'max': 0.50, 'strategy': 'risk'},
{'name': 'max_holding_minutes', 'type': 'int', 'min': 30, 'max': 180, 'strategy': 'risk'},
{'name': 'min_profit_for_time_exit', 'type': 'float', 'min': 0.002, 'max': 0.01, 'strategy': 'risk'},
# ★ Strategy Weights — 这些基因现在真正影响适应度
{'name': 'weight_MACrossStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_RSIStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_BollingerStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_MeanReversionStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_MomentumBreakoutStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_MACDStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_KDJStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_TurtleStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_DailyBreakoutStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
{'name': 'weight_WaveTheoryStrategy', 'type': 'float', 'min': 0.0, 'max': 2.0, 'strategy': 'weight'},
# Market State Analysis parameters
{'name': 'market_trend_period', 'type': 'int', 'min': 20, 'max': 100, 'strategy': 'market_state'},
{'name': 'market_retracement_tolerance', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'market_state'},
{'name': 'market_volume_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'market_state'},
{'name': 'market_volume_ma_period', 'type': 'int', 'min': 5, 'max': 30, 'strategy': 'market_state'},
# Trend Indicator Weights
{'name': 'trend_price_breakout_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
{'name': 'trend_volume_confirmation_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
{'name': 'trend_momentum_oscillator_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
{'name': 'trend_moving_average_weight', 'type': 'float', 'min': 0.1, 'max': 0.5, 'strategy': 'trend_weights'},
# Trend Thresholds
{'name': 'trend_strong_threshold', 'type': 'float', 'min': 0.4, 'max': 0.8, 'strategy': 'trend_thresholds'},
{'name': 'trend_weak_threshold', 'type': 'float', 'min': 0.2, 'max': 0.5, 'strategy': 'trend_thresholds'},
{'name': 'trend_volume_spike', 'type': 'float', 'min': 1.0, 'max': 3.0, 'strategy': 'trend_thresholds'},
{'name': 'trend_oversold', 'type': 'int', 'min': 20, 'max': 40, 'strategy': 'trend_thresholds'},
{'name': 'trend_overbought', 'type': 'int', 'min': 60, 'max': 80, 'strategy': 'trend_thresholds'},
# Confidence Thresholds
{'name': 'confidence_high', 'type': 'float', 'min': 0.5, 'max': 0.9, 'strategy': 'confidence'},
{'name': 'confidence_medium', 'type': 'float', 'min': 0.3, 'max': 0.7, 'strategy': 'confidence'},
]
# class_name → config_key 映射(从 StrategyRegistry 获取)
_class_name_to_config_key = {
"MACrossStrategy": "ma_cross",
"RSIStrategy": "rsi",
"BollingerStrategy": "bollinger",
"MeanReversionStrategy": "mean_reversion",
"MomentumBreakoutStrategy": "momentum_breakout",
"MACDStrategy": "macd",
"KDJStrategy": "kdj",
"TurtleStrategy": "turtle",
"DailyBreakoutStrategy": "daily_breakout",
"WaveTheoryStrategy": "wave_theory",
}
def _parse_individual(individual):
"""解析个体基因为命名参数字典,并裁剪到定义范围"""
parsed = {}
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
val = individual[idx]
if param_def['type'] == 'int':
val = int(round(val))
# 裁剪到 [min, max] 防止变异越界
val = max(param_def['min'], min(param_def['max'], val))
parsed[param_def['name']] = val
return parsed
def _extract_strategy_params(parsed: dict) -> dict:
"""从解析后的基因提取策略参数字典"""
strategy_params = {}
for param_def in PARAMETER_DEFINITIONS:
strategy_name = param_def.get('strategy')
if strategy_name in ('weight', 'signal', 'risk', 'market_state',
'trend_weights', 'trend_thresholds', 'confidence'):
continue
if strategy_name not in strategy_params:
strategy_params[strategy_name] = {}
# 参数名映射(保持与旧版兼容)
param_name = param_def['name']
if param_name == 'ma_cross_short_window':
param_name = 'short_window'
elif param_name == 'ma_cross_long_window':
param_name = 'long_window'
elif param_name == 'rsi_period':
param_name = 'period'
elif param_name == 'rsi_overbought':
param_name = 'overbought'
elif param_name == 'rsi_oversold':
param_name = 'oversold'
elif param_name == 'bollinger_period':
param_name = 'period'
elif param_name == 'bollinger_std_dev':
param_name = 'std_dev'
elif param_name == 'macd_fast_ema':
param_name = 'fast_ema'
elif param_name == 'macd_slow_ema':
param_name = 'slow_ema'
elif param_name == 'macd_signal_period':
param_name = 'signal_period'
elif param_name == 'mean_reversion_period':
param_name = 'period'
elif param_name == 'mean_reversion_std_dev':
param_name = 'std_dev'
elif param_name == 'momentum_breakout_period':
param_name = 'period'
elif param_name == 'momentum_breakout_momentum_period':
param_name = 'momentum_period'
elif param_name == 'kdj_period':
param_name = 'period'
elif param_name == 'turtle_period':
param_name = 'period'
elif param_name.startswith("wave_"):
param_name = param_name[5:]
if param_name == "period":
param_name = "wave_period"
elif param_name == 'daily_breakout_bars_count':
param_name = 'bars_count'
strategy_params[strategy_name][param_name] = parsed[param_def['name']]
return strategy_params
def _extract_weight_genes(parsed: dict) -> dict:
"""★ 提取权重基因 → {config_key: weight}
这些权重会通过 individual_weights 参数传递给
MarketStateAnalyzer.get_strategy_weights()使权重基因真正生效
"""
weight_genes = {}
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') != 'weight':
continue
class_name = param_def['name'].replace('weight_', '')
config_key = _class_name_to_config_key.get(class_name, class_name.lower())
weight_genes[config_key] = parsed[param_def['name']]
return weight_genes
def _precompute_h1_states(multi_tf: MultiTimeframeDataStore, analyzer: MarketStateAnalyzer) -> list:
"""预计算H1市场状态序列 → 映射到M1时间轴"""
h1_df = multi_tf.ensure_timeframe(PERIOD_H1)
if h1_df is None or len(h1_df) == 0:
return [("none", 0.0)] * multi_tf.length
m1_index = multi_tf.main_df.index
h1_index = h1_df.index
# 计算每个H1 bar的状态
h1_states = []
for i in range(len(h1_df)):
lookback = analyzer.trend_period + 10
start = max(0, i - lookback)
df_slice = h1_df.iloc[start:i + 1]
state, conf = analyzer._calculate_state_from_df(df_slice)
h1_states.append((state, conf))
# 映射到M1时间轴
m1_states = []
h1_pos = 0
first_h1_time = h1_index[0]
for m1_time in m1_index:
if m1_time < first_h1_time:
m1_states.append(("none", 0.0))
continue
while h1_pos < len(h1_index) - 1 and m1_time >= h1_index[h1_pos + 1]:
h1_pos += 1
if h1_pos < len(h1_states):
m1_states.append(h1_states[h1_pos])
else:
m1_states.append(("none", 0.0))
return m1_states
def evaluate_fitness(individual, multi_tf: MultiTimeframeDataStore,
_signals_df_unused=None) -> tuple:
"""★ 重写的适应度函数 — 所有权重基因、策略参数和风控参数真正生效
三个关键基因全部生效
1. 策略参数 重新实例化策略并 run_backtest 影响信号序列
2. 权重基因 per-bar get_strategy_weights(individual_weights=...) 影响信号组合
3. 风控参数 PositionManager 在回测中真正使用
"""
parsed = _parse_individual(individual)
# 1. 提取策略参数
strategy_params = _extract_strategy_params(parsed)
# 2. ★ 提取权重基因(核心修复)
weight_genes = _extract_weight_genes(parsed)
# 3. 提取风控参数
risk_params = {
'stop_loss_pct': parsed.get('stop_loss_pct', RISK_CONFIG.get('stop_loss_pct', -0.01)),
'profit_retracement_pct': parsed.get('profit_retracement_pct', RISK_CONFIG.get('profit_retracement_pct', 0.10)),
'min_profit_for_trailing': parsed.get('min_profit_for_trailing', RISK_CONFIG.get('min_profit_for_trailing', 0.01)),
'take_profit_pct': parsed.get('take_profit_pct', RISK_CONFIG.get('take_profit_pct', 0.20)),
'max_holding_minutes': parsed.get('max_holding_minutes', RISK_CONFIG.get('max_holding_minutes', 60)),
'min_profit_for_time_exit': parsed.get('min_profit_for_time_exit', RISK_CONFIG.get('min_profit_for_time_exit', 0.005)),
'max_daily_loss': parsed.get('max_daily_loss', RISK_CONFIG.get('max_daily_loss', -0.30)),
}
# 4. 提取市场状态和趋势参数
market_params = {
'trend_period': parsed.get('market_trend_period', MARKET_STATE_CONFIG.get('trend_period', 50)),
'retracement_tolerance': parsed.get('market_retracement_tolerance', MARKET_STATE_CONFIG.get('retracement_tolerance', 0.30)),
'volume_period': parsed.get('market_volume_period', MARKET_STATE_CONFIG.get('volume_period', 20)),
'volume_ma_period': parsed.get('market_volume_ma_period', MARKET_STATE_CONFIG.get('volume_ma_period', 10)),
'hourly_data_count': 100,
}
trend_weights = {
'price_breakout': parsed.get('trend_price_breakout_weight', 0.35),
'volume_confirmation': parsed.get('trend_volume_confirmation_weight', 0.25),
'momentum oscillator': parsed.get('trend_momentum_oscillator_weight', 0.20),
'moving_average': parsed.get('trend_moving_average_weight', 0.20),
}
trend_thresholds = {
'strong_trend': parsed.get('trend_strong_threshold', 0.6),
'weak_trend': parsed.get('trend_weak_threshold', 0.3),
'volume_spike': parsed.get('trend_volume_spike', 1.5),
'oversold': parsed.get('trend_oversold', 30),
'overbought': parsed.get('trend_overbought', 70),
}
confidence_thresholds = {
'high_confidence': parsed.get('confidence_high', 0.7),
'medium_confidence': parsed.get('confidence_medium', 0.4),
}
# 5. 构建 MarketStateAnalyzer 并预计算H1状态
analyzer = MarketStateAnalyzer(
timeframe=PERIOD_H1,
market_state_params=market_params,
trend_weights=trend_weights,
trend_thresholds=trend_thresholds,
confidence_thresholds=confidence_thresholds,
)
analyzer._precomputed_states = _precompute_h1_states(multi_tf, analyzer)
# 5.5. ★ 使用个体的策略参数重新实例化策略并重新计算信号
# 将 class_name → {params} 映射为 config_key → {params}
strategy_params_by_key = {
_class_name_to_config_key.get(cn, cn.lower()): params
for cn, params in strategy_params.items()
}
registry = StrategyRegistry()
sig_instances = registry.instantiate_all(SYMBOL, TIMEFRAME, strategy_params_by_key)
signals_df = pd.DataFrame(index=multi_tf.main_df.index)
for config_key, strat in sig_instances.items():
try:
sig = strat.run_backtest(multi_tf.main_df)
signals_df[config_key] = sig if sig is not None else pd.Series(0, index=signals_df.index)
except Exception:
signals_df[config_key] = pd.Series(0, index=signals_df.index)
# 6. 回测环境
data_provider = BacktestDataProvider(multi_tf, INITIAL_CAPITAL)
pm = PositionManager(data_provider, trade_direction="both", risk_config=risk_params,
persist_peaks=False)
buy_th = parsed.get('buy_threshold', 1.5)
sell_th = parsed.get('sell_threshold', -1.5)
total_bars = multi_tf.length
# 7. ★ 逐 bar 回测(权重基因真正影响信号组合)
for bar_index in range(total_bars):
current_price = data_provider.get_current_price(SYMBOL)
if not current_price:
data_provider.tick()
continue
# 获取当前 bar 的市场状态
state, conf = analyzer.get_market_state(bar_index)
# ★ 权重基因在这里生效
weights = analyzer.get_strategy_weights(
state, conf, individual_weights=weight_genes
)
# 组合当前 bar 的信号(使用个体参数重新计算的信号)
bar_signals = {
col: signals_df[col].iloc[bar_index]
for col in signals_df.columns
}
final_signal = SignalCombiner.combine_at_bar(bar_signals, weights, buy_th, sell_th)
# 执行交易
if final_signal == 1:
pm.open_position("buy", current_price, 1.0, dry_run=True)
elif final_signal == -1:
pm.open_position("sell", current_price, 1.0, dry_run=True)
pm.monitor_positions(current_price, dry_run=True)
data_provider.tick()
# 8. 适应度 = 总盈亏
total_pnl = pm.total_equity - INITIAL_CAPITAL
return (total_pnl,)
def load_historical_data():
"""一次性加载M1数据并返回 MultiTimeframeDataStore"""
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 or len(rates) == 0:
raise RuntimeError("获取历史数据失败")
if len(rates) > OPTIMIZER_COUNT:
rates = rates[-OPTIMIZER_COUNT:]
store = MultiTimeframeDataStore()
store.load_m1_data(rates)
# 预生成H1数据
store.ensure_timeframe(PERIOD_H1)
return store
def precompute_all_signals(multi_tf: MultiTimeframeDataStore,
strategy_params: dict = None) -> pd.DataFrame:
"""使用默认参数预计算所有策略的回测信号"""
registry = StrategyRegistry()
strategies = registry.instantiate_all(SYMBOL, TIMEFRAME, strategy_params)
signals_df = pd.DataFrame(index=multi_tf.main_df.index)
for config_key, strategy in strategies.items():
try:
sig = strategy.run_backtest(multi_tf.main_df)
signals_df[config_key] = sig if sig is not None else pd.Series(0, index=signals_df.index)
except Exception:
signals_df[config_key] = pd.Series(0, index=signals_df.index)
return signals_df
def run_optimizer():
"""运行遗传算法优化(所有基因真正生效)"""
try:
# 1. 加载数据(一次性)
logger.info("加载历史数据...")
multi_tf = load_historical_data()
logger.info(f"数据加载完成: {multi_tf.length} 条M1数据")
# 2. DEAP 设置(策略信号在 evaluate_fitness 内部逐个体重新计算)
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()
for i, param_def in enumerate(PARAMETER_DEFINITIONS):
if param_def['type'] == 'int':
toolbox.register(f"attr_param_{i}", random.randint, param_def['min'], param_def['max'])
else:
toolbox.register(f"attr_param_{i}", random.uniform, param_def['min'], param_def['max'])
toolbox.register("individual", tools.initIterate, creator.Individual,
lambda: [toolbox.__getattribute__(f"attr_param_{j}")()
for j in range(len(PARAMETER_DEFINITIONS))])
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("evaluate", evaluate_fitness,
multi_tf=multi_tf)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutGaussian,
mu=GENETIC_OPTIMIZER_CONFIG["mutation_mu"],
sigma=GENETIC_OPTIMIZER_CONFIG["mutation_sigma"],
indpb=GENETIC_OPTIMIZER_CONFIG["mutation_indpb"])
toolbox.register("select", tools.selTournament,
tournsize=GENETIC_OPTIMIZER_CONFIG["tournament_size"])
# 多进程
if GENETIC_OPTIMIZER_CONFIG["enable_multiprocessing"]:
pool = multiprocessing.Pool(
processes=GENETIC_OPTIMIZER_CONFIG["processes"],
initializer=init_worker
)
toolbox.register("map", pool.map)
else:
toolbox.register("map", map)
# 4. 统计
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)
# 5. 运行
population = toolbox.population(n=GENETIC_OPTIMIZER_CONFIG["population_size"])
ngen = GENETIC_OPTIMIZER_CONFIG["generations"]
cxpb = GENETIC_OPTIMIZER_CONFIG["crossover_probability"]
mutpb = GENETIC_OPTIMIZER_CONFIG["mutation_probability"]
logger.info(f"开始进化: 种群 {len(population)}, {ngen}")
generation_info = []
for gen in range(ngen):
offspring = toolbox.select(population, len(population))
offspring = algorithms.varAnd(offspring, toolbox, cxpb, mutpb)
fits = toolbox.map(toolbox.evaluate, offspring)
for fit, ind in zip(fits, offspring):
ind.fitness.values = fit
population[:] = offspring
best = tools.selBest(population, k=1)[0]
best_fit = best.fitness.values[0]
avg_fit = np.mean([ind.fitness.values[0] for ind in population])
if GENETIC_OPTIMIZER_CONFIG.get("save_generation_info", True):
generation_info.append({
'generation': gen + 1,
'best_fitness': best_fit,
'avg_fitness': avg_fit,
})
if GENETIC_OPTIMIZER_CONFIG.get("verbose", True):
logger.info(f"代数 {gen + 1}/{ngen}: 最佳={best_fit:.2f}, 平均={avg_fit:.2f}")
# 6. 结果
if GENETIC_OPTIMIZER_CONFIG["enable_multiprocessing"]:
pool.close()
pool.join()
best_individual = tools.selBest(population, k=1)[0]
best_fitness = best_individual.fitness.values[0]
logger.info(f"优化完成: 最佳适应度={best_fitness:.2f}")
save_optimization_results(best_individual, best_fitness, generation_info)
return _parse_individual(best_individual), best_fitness
except Exception as e:
import traceback
logger.error(f"优化器错误: {e}\n{traceback.format_exc()}")
raise
def save_optimization_results(best_individual, best_fitness, generation_info):
"""保存优化结果"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
parsed = _parse_individual(best_individual)
# JSON
import json
results = {
'best_fitness': best_fitness,
'best_params': parsed,
'generation_info': generation_info,
}
json_path = f"optimization_results_{timestamp}.json"
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2, default=str)
logger.info(f"结果已保存: {json_path}")
# TXT报告
txt_path = f"optimization_report_{timestamp}.txt"
with open(txt_path, 'w', encoding='utf-8') as f:
f.write(f"优化完成时间: {datetime.now()}\n")
f.write(f"最佳适应度: {best_fitness:.2f}\n\n")
f.write("最佳参数:\n")
for key, value in parsed.items():
f.write(f" {key}: {value}\n")
f.write("\n代数统计:\n")
for gi in generation_info:
f.write(f" 代数 {gi['generation']}: 最佳={gi['best_fitness']:.2f}, 平均={gi['avg_fitness']:.2f}\n")
logger.info(f"报告已保存: {txt_path}")
if __name__ == "__main__":
run_optimizer()
+1 -1
View File
@@ -5,7 +5,7 @@ from datetime import datetime
from logger import logger
from config import SYMBOL, TIMEFRAME, REALTIME_CONFIG, SIGNAL_THRESHOLDS
from core.risk import RiskController
from execution.dynamic_weights import DynamicWeightManager
from execution.weights import DynamicWeightManager
class RealtimeTrader:
"""实时交易器 (已重构为依赖注入)"""
+78
View File
@@ -0,0 +1,78 @@
"""动态权重管理器(重写版)
关键改进
1. 使用 StrategyRegistry单例获取策略列表消除 blueprint 重复
2. 回测/优化器模式支持逐 bar 获取权重get_weights_for_bar
3. 优化器模式接受 individual_weights 参数并传递给 MarketStateAnalyzer
"""
import strategies # noqa: F401 — 触发策略注册
from logger import logger
from core.risk.market_state import MarketStateAnalyzer
from core.signal.registry import StrategyRegistry
from config import SYMBOL, TIMEFRAME
class DynamicWeightManager:
"""动态权重管理器"""
def __init__(self, data_provider,
market_state_analyzer: MarketStateAnalyzer = None):
self.data_provider = data_provider
self.analyzer = market_state_analyzer or MarketStateAnalyzer(data_provider)
self.registry = StrategyRegistry()
# 初始化策略实例(仅实盘模式首次使用)
self._strategies_initialized = False
def _ensure_strategies(self):
if not self._strategies_initialized:
self.registry.instantiate_all(SYMBOL, TIMEFRAME,
data_provider=self.data_provider)
self._strategies_initialized = True
# ── 实盘模式 ──
def get_current_strategies_and_weights(self) -> list:
"""获取当前(实时)策略实例和权重列表"""
self._ensure_strategies()
weights = self.get_current_weights()
result = []
for config_key, weight in weights.items():
instance = self.registry.get_instance(config_key)
if instance is not None:
result.append((instance, weight))
return result
def get_current_weights(self) -> dict:
"""获取当前实时权重"""
market_state, confidence = self.analyzer.get_market_state()
return self.analyzer.get_strategy_weights(market_state, confidence)
# ── 回测/优化器模式 ──
def get_weights_for_bar(self, bar_index: int,
individual_weights: dict = None) -> dict:
"""获取第 bar_index 个 bar 的策略权重
Args:
bar_index: 当前M1 bar索引
individual_weights: 优化器传入 {config_key: weight}非None时直接使用
Returns:
{config_key: weight}
"""
market_state, confidence = self.analyzer.get_market_state(bar_index)
return self.analyzer.get_strategy_weights(
market_state, confidence, individual_weights
)
def get_weight_info(self) -> dict:
market_state, confidence = self.analyzer.get_market_state()
market_weights = self.analyzer.get_strategy_weights(market_state, confidence)
return {
'market_state': market_state,
'confidence': confidence,
'weights': market_weights
}
-22
View File
@@ -1,22 +0,0 @@
import pandas as pd
from core.utils import initialize, shutdown, get_rates
from logger import logger
from optimizer import run_optimizer
# Note: The run_backtest and run_realtime functions have been moved to
# start_backtest.py and start_realtime.py respectively.
# This file is kept as a reference for the optimizer entry point.
if __name__ == "__main__":
# --- 选择运行模式 ---
# 1. 运行回测, 请执行 python start_backtest.py
# run_backtest()
# 2. 运行实盘交易, 请执行 python start_realtime.py
# from realtime_trader import RealtimeTrader
# trader = RealtimeTrader()
# trader.start()
# 3. 运行遗传算法优化,寻找最佳权重
# run_optimizer()
pass
-721
View File
@@ -1,721 +0,0 @@
import random
import numpy as np
from deap import base, creator, tools, algorithms
import multiprocessing
import pandas as pd
import sys
import os
from datetime import datetime
from tqdm import tqdm
# 固定随机种子,确保可复现
SEED = 42
random.seed(SEED)
np.random.seed(SEED)
# 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
# 模块级别的初始化函数,用于多进程
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_historical_data
# --- Parameter Definition for DEAP Individual ---
# Define the search space for strategy parameters and weights
# Each entry: (name, type, min_val, max_val, strategy_class_name_if_param, default_val_if_weight)
# Type 'int' for integer parameters, 'float' for float parameters/weights
# Order matters for parsing the individual
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 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
}
def evaluate_fitness(individual, df_data):
parsed_params = {}
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
val = individual[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 != 'weight' and strategy_name != 'signal' and strategy_name != 'risk':
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:] # 移除 "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,)
# 使用动态权重管理器获取策略权重
strategies_with_weights = dynamic_weight_manager.get_current_strategies_and_weights()
# numpy加权计算
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,)
# --- Main Optimization Run ---
def run_optimizer():
try:
# Load data once
df_historical_data = load_historical_data()
# DEAP setup
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
creator.create("Individual", list, fitness=creator.FitnessMax)
toolbox = base.Toolbox()
# Register attribute generators for each parameter
for i, param_def in enumerate(PARAMETER_DEFINITIONS):
if param_def['type'] == 'int':
toolbox.register(f"attr_param_{i}", random.randint, param_def['min'], param_def['max'])
else: # float
toolbox.register(f"attr_param_{i}", random.uniform, param_def['min'], param_def['max'])
# Register individual creator
toolbox.register("individual", tools.initIterate, creator.Individual,
lambda: [toolbox.__getattribute__(f"attr_param_{j}")() for j in range(len(PARAMETER_DEFINITIONS))])
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
toolbox.register("evaluate", evaluate_fitness, df_data=df_historical_data)
toolbox.register("mate", tools.cxTwoPoint)
toolbox.register("mutate", tools.mutGaussian,
mu=GENETIC_OPTIMIZER_CONFIG["mutation_mu"],
sigma=GENETIC_OPTIMIZER_CONFIG["mutation_sigma"],
indpb=GENETIC_OPTIMIZER_CONFIG["mutation_indpb"])
toolbox.register("select", tools.selTournament,
tournsize=GENETIC_OPTIMIZER_CONFIG["tournament_size"])
# 多进程处理
if GENETIC_OPTIMIZER_CONFIG["enable_multiprocessing"]:
pool = multiprocessing.Pool(processes=GENETIC_OPTIMIZER_CONFIG["processes"],
initializer=init_worker)
toolbox.register("map", pool.map)
else:
# 不使用多进程时,使用普通的map函数
toolbox.register("map", map)
# Statistics setup
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("std", np.std)
stats.register("min", np.min)
stats.register("max", np.max)
# Run algorithm - 从配置获取参数
population = toolbox.population(n=GENETIC_OPTIMIZER_CONFIG["population_size"])
ngen = GENETIC_OPTIMIZER_CONFIG["generations"]
cxpb = GENETIC_OPTIMIZER_CONFIG["crossover_probability"]
mutpb = GENETIC_OPTIMIZER_CONFIG["mutation_probability"]
if GENETIC_OPTIMIZER_CONFIG["verbose"]:
print(f"开始多进程进化... (种群大小: {len(population)}, 进化代数: {ngen})")
# 自定义统计回调以打印代数信息
generation_info = []
def generation_callback(gen, population):
best_gen_individual = tools.selBest(population, k=1)[0]
best_gen_fitness = best_gen_individual.fitness.values[0]
avg_gen_fitness = sum(ind.fitness.values[0] for ind in population) / len(population)
# 解析最佳个体的参数
parsed_params = {}
for idx, param_def in enumerate(PARAMETER_DEFINITIONS):
param_value = best_gen_individual[idx]
if param_def['type'] == 'int':
parsed_params[param_def['name']] = int(round(param_value))
else:
parsed_params[param_def['name']] = param_value
# 根据配置决定是否保存代数信息
if GENETIC_OPTIMIZER_CONFIG["save_generation_info"]:
generation_info.append({
'generation': gen,
'best_fitness': best_gen_fitness,
'avg_fitness': avg_gen_fitness,
'best_params': parsed_params.copy()
})
# 根据配置决定是否输出详细信息
if GENETIC_OPTIMIZER_CONFIG["verbose"]:
print(f"\n=== 代数 {gen} ===")
print(f"最佳适应度 = {best_gen_fitness:.2f}, 平均适应度 = {avg_gen_fitness:.2f}")
print("当前最佳参数组合:")
for key, value in parsed_params.items():
print(f" {key}: {value}")
# 使用自定义进化循环
for gen in range(ngen):
offspring = toolbox.select(population, len(population))
offspring = algorithms.varAnd(offspring, toolbox, cxpb, mutpb)
fits = toolbox.map(toolbox.evaluate, offspring)
for fit, ind in zip(fits, offspring):
ind.fitness.values = fit
population[:] = offspring
# 调用代数信息回调
generation_callback(gen + 1, population)
# 关闭进程池(如果使用了多进程)
if GENETIC_OPTIMIZER_CONFIG["enable_multiprocessing"]:
pool.close()
pool.join() # Wait for all processes to finish
# Results
best_individual = tools.selBest(population, k=1)[0]
best_fitness = best_individual.fitness.values[0]
print("--- DEAP 优化结束 ---")
print(f"最佳适应度 (总盈亏): {best_fitness:.2f}")
print("最佳参数组合:")
# 打印详细的代数信息
print("\n=== 代数统计信息 ===")
for gen_info in generation_info:
print(f"代数 {gen_info['generation']}: 最佳={gen_info['best_fitness']:.2f}, 平均={gen_info['avg_fitness']:.2f}")
print("\n=== 运行结果总结 ===")
print(f"总进化代数: {ngen}")
print(f"种群大小: {len(population)}")
print(f"交叉概率: {cxpb}")
print(f"变异概率: {mutpb}")
print(f"最终最佳适应度: {best_fitness:.2f}")
# 解析最佳参数组合
parsed_best_params = {}
current_idx = 0
for param_def in PARAMETER_DEFINITIONS:
param_value = best_individual[current_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
current_idx += 1
print("\n=== 最终最佳参数组合 ===")
# 分组显示参数:策略参数和权重
print("\n策略参数:")
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') != 'weight':
print(f" {param_def['name']}: {parsed_best_params[param_def['name']]}")
print("\n策略权重:")
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') == 'weight':
strategy_name = param_def['name'].replace('weight_', '')
print(f" {strategy_name}: {parsed_best_params[param_def['name']]:.3f}")
# 显示权重总和
weight_sum = sum(parsed_best_params[p['name']] for p in PARAMETER_DEFINITIONS if p.get('strategy') == 'weight')
print(f"\n权重总和: {weight_sum:.3f}")
# 显示历史最佳参数变化
print("\n=== 历史最佳参数变化 ===")
for i, gen_info in enumerate(generation_info):
if i % 5 == 0 or i == len(generation_info) - 1: # 每5代显示一次,包括最后一代
print(f"代数 {gen_info['generation']}: 适应度={gen_info['best_fitness']:.2f}")
# 只显示权重信息
weights = {k: v for k, v in gen_info['best_params'].items() if k.startswith('weight_')}
for weight_key, weight_value in weights.items():
strategy_name = weight_key.replace('weight_', '')
print(f" {strategy_name}: {weight_value:.3f}")
print()
# 保存优化结果到文件
save_optimization_results(parsed_best_params, best_fitness, generation_info)
return parsed_best_params, best_fitness
except Exception as e:
print(f"优化器运行出错: {e}")
import traceback
print(traceback.format_exc())
# Ensure pool is terminated on error, only if it was created
if 'pool' in locals() and pool is not None:
pool.terminate()
sys.exit(1)
def save_optimization_results(best_params, best_fitness, generation_info):
"""保存优化结果到文件"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 创建结果数据结构
results = {
'optimization_info': {
'timestamp': timestamp,
'best_fitness': float(best_fitness),
'total_generations': len(generation_info),
'population_size': GENETIC_OPTIMIZER_CONFIG["population_size"],
'crossover_probability': GENETIC_OPTIMIZER_CONFIG["crossover_probability"],
'mutation_probability': GENETIC_OPTIMIZER_CONFIG["mutation_probability"],
'random_seed': SEED
},
'best_parameters': best_params,
'generation_history': generation_info
}
# 保存为JSON文件
json_filename = f"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"optimization_best_params_{timestamp}.csv"
import 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}")
# 保存代数历史为CSV
history_csv_filename = f"optimization_history_{timestamp}.csv"
with open(history_csv_filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Generation', 'Best_Fitness', 'Avg_Fitness'] +
[p['name'] for p in PARAMETER_DEFINITIONS])
for gen_info in generation_info:
row = [
gen_info['generation'],
gen_info['best_fitness'],
gen_info['avg_fitness']
]
# 添加所有参数值
for param_def in PARAMETER_DEFINITIONS:
row.append(gen_info['best_params'].get(param_def['name'], ''))
writer.writerow(row)
print(f"代数历史已保存到: {history_csv_filename}")
# 生成可读的文本报告
txt_filename = f"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"最佳适应度 (总盈亏): ${best_fitness:.2f}\n")
f.write(f"总进化代数: {len(generation_info)}\n")
f.write(f"种群大小: 50\n")
f.write(f"随机种子: {SEED}\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']:
param_name = param_def['name']
param_value = best_params[param_name]
f.write(f" {param_name}: {param_value}\n")
f.write("\n【信号阈值】\n")
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') == 'signal':
param_name = param_def['name']
param_value = best_params[param_name]
f.write(f" {param_name}: {param_value}\n")
f.write("\n【风险管理参数】\n")
for param_def in PARAMETER_DEFINITIONS:
if param_def.get('strategy') == 'risk':
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")
for i, gen_info in enumerate(generation_info):
if i % 5 == 0 or i == len(generation_info) - 1:
f.write(f"\n代数 {gen_info['generation']}:\n")
f.write(f" 最佳适应度: ${gen_info['best_fitness']:.2f}\n")
f.write(f" 平均适应度: ${gen_info['avg_fitness']:.2f}\n")
print(f"详细报告已保存到: {txt_filename}")
except Exception as e:
print(f"保存优化结果时出错: {e}")
import traceback
print(traceback.format_exc())
if __name__ == "__main__":
run_optimizer()
+4 -1
View File
@@ -1,4 +1,7 @@
MetaTrader5
pandas
deap
tqdm
tqdm
fastapi
uvicorn
requests
View File
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""回测入口 — python -m run.backtest"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.utils import get_rates, initialize, shutdown
from execution.backtest import BacktestEngine
from config import SYMBOL, TIMEFRAME, BACKTEST_COUNT, BACKTEST_START_DATE, BACKTEST_END_DATE, USE_DATE_RANGE
from logger import logger
def main():
print("=" * 60)
print("MT5 智能交易系统 - 回测")
print("=" * 60)
try:
initialize()
if USE_DATE_RANGE:
rates = get_rates(SYMBOL, TIMEFRAME, BACKTEST_COUNT,
BACKTEST_START_DATE, BACKTEST_END_DATE)
else:
rates = get_rates(SYMBOL, TIMEFRAME, BACKTEST_COUNT)
shutdown()
if rates is None or len(rates) == 0:
logger.error("未能获取历史数据,回测终止。")
return
logger.info(f"获取数据: {len(rates)}")
engine = BacktestEngine(use_dynamic_weights=False)
summary = engine.run(rates)
print(f"\n回测完成!总盈亏: ${summary.get('total_profit_loss', 0):.2f}")
except Exception as e:
import traceback
logger.error(f"回测出错: {e}")
traceback.print_exc()
if __name__ == "__main__":
main()
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""优化器入口 — python -m run.optimize"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from execution.optimize import run_optimizer
from logger import logger
def main():
print("=" * 60)
print("MT5 智能交易系统 - 遗传算法参数优化")
print("=" * 60)
try:
best_params, best_fitness = run_optimizer()
print(f"\n优化完成!最佳适应度: {best_fitness:.2f}")
except Exception as e:
import traceback
logger.error(f"优化器运行出错: {e}")
traceback.print_exc()
if __name__ == "__main__":
main()
+10 -17
View File
@@ -1,52 +1,45 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
实时交易启动脚本 (已重构)
"""
"""实时交易入口 — python -m run.realtime"""
import sys
import os
from datetime import datetime
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from execution.realtime_trader import RealtimeTrader
from core.data_providers import LiveDataProvider, DryRunDataProvider
from core.data import LiveDataProvider, DryRunDataProvider
from config import REALTIME_CONFIG, INITIAL_CAPITAL
from logger import setup_logger
def main():
# 设置日志级别
log_level = REALTIME_CONFIG.get('logging_level', 'INFO')
setup_logger(log_level)
# 从配置决定运行模式
is_dry_run = REALTIME_CONFIG.get('dry_run', True)
print("=" * 60)
print("MetaTrader 5 智能交易系统")
print("MT5 智能交易系统 - 实时交易")
print(f"启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"运行模式: {'模拟运行' if is_dry_run else '实盘交易'}")
print("=" * 60)
# 根据模式选择数据提供者
if is_dry_run:
data_provider = DryRunDataProvider(initial_equity=INITIAL_CAPITAL)
else:
# 安全确认
print("⚠️ 警告:即将启动实盘交易模式!")
print("WARNING: 即将启动实盘交易模式!")
confirm = input("确认启动实盘交易?(输入 'YES' 继续): ")
if confirm != 'YES':
print("已取消启动。")
return
data_provider = LiveDataProvider()
# 创建并启动交易器实例
trader = RealtimeTrader(data_provider, update_interval=REALTIME_CONFIG['update_interval'])
print("\n正在启动交易系统... (按 Ctrl+C 可安全停止)")
trader.start()
if __name__ == "__main__":
main()
main()
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""MT5 代理服务 — python -m run.server"""
import sys
import os
import threading
from contextlib import asynccontextmanager
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from core.data import LiveDataProvider
from core.data.serializers import (
serialize_account_info,
serialize_position,
serialize_symbol_info,
serialize_order_result,
serialize_rates,
)
from config import SERVER_HOST, SERVER_PORT
from logger import setup_logger, logger
setup_logger("INFO")
# 全局 MT5 连接和线程锁
_provider = LiveDataProvider()
_lock = threading.Lock()
@asynccontextmanager
async def lifespan(app: FastAPI):
with _lock:
if not _provider.initialize():
logger.error("MT5 初始化失败,服务启动中止")
sys.exit(1)
logger.info(f"MT5 代理服务启动于 {SERVER_HOST}:{SERVER_PORT}")
yield
with _lock:
_provider.shutdown()
logger.info("MT5 代理服务已关闭")
app = FastAPI(title="MT5 Proxy API", lifespan=lifespan)
# ── 请求模型 ──
class OrderRequest(BaseModel):
symbol: str
order_type: str
volume: float
class CloseRequest(BaseModel):
ticket: int
symbol: str
volume: float
# ── 端点 ──
@app.post("/api/initialize")
def api_initialize():
with _lock:
return {"success": _provider.initialize()}
@app.post("/api/shutdown")
def api_shutdown():
with _lock:
_provider.shutdown()
return {"success": True}
@app.get("/api/price/{symbol}")
def api_price(symbol: str):
with _lock:
data = _provider.get_current_price(symbol)
if data is None:
return JSONResponse(status_code=503, content={"error": "无法获取价格"})
# time 转为 Unix 时间戳
data['time'] = int(data['time'].timestamp()) if hasattr(data['time'], 'timestamp') else int(data['time'])
return data
@app.get("/api/historical/{symbol}/{timeframe}/{count}")
def api_historical(symbol: str, timeframe: int, count: int):
with _lock:
rates = _provider.get_historical_data(symbol, timeframe, count)
if rates is None:
return JSONResponse(status_code=503, content={"error": "无法获取历史数据"})
return {"rates": serialize_rates(rates)}
@app.get("/api/account")
def api_account():
with _lock:
info = _provider.get_account_info()
if info is None:
return JSONResponse(status_code=503, content={"error": "无法获取账户信息"})
return serialize_account_info(info)
@app.get("/api/positions/{symbol}")
def api_positions(symbol: str):
with _lock:
positions = _provider.get_positions(symbol)
if positions is None:
return {"positions": []}
return {"positions": [serialize_position(p) for p in positions]}
@app.get("/api/symbol/{symbol}")
def api_symbol(symbol: str):
with _lock:
info = _provider.get_symbol_info(symbol)
if info is None:
return JSONResponse(status_code=503, content={"error": "无法获取品种信息"})
return serialize_symbol_info(info)
@app.post("/api/order")
def api_order(req: OrderRequest):
with _lock:
result = _provider.send_order(req.symbol, req.order_type, req.volume)
if result is None:
return JSONResponse(status_code=503, content={"error": "下单失败"})
return serialize_order_result(result)
@app.post("/api/close")
def api_close(req: CloseRequest):
with _lock:
success = _provider.close_position(req.ticket, req.symbol, req.volume)
return {"success": success}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=SERVER_HOST, port=SERVER_PORT)
-229
View File
@@ -1,229 +0,0 @@
import sys
import os
import pandas as pd
from datetime import datetime
from tqdm import tqdm
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__name__)))
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 logger import logger
# 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
def run_full_backtest():
"""完整的、独立的、已重构的回测流程"""
# 1. 加载数据
from core.utils import get_rates, initialize, shutdown
initialize()
if USE_DATE_RANGE:
rates = get_rates(SYMBOL, TIMEFRAME, BACKTEST_COUNT, BACKTEST_START_DATE, BACKTEST_END_DATE)
else:
rates = get_rates(SYMBOL, TIMEFRAME, BACKTEST_COUNT)
shutdown()
if rates is None:
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, {})
}
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"
}
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,不中断整个回测
continue
# 5. 结束和报告 (Keep as is)
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)}")
def main():
print("=" * 60)
print("MetaTrader 5 智能交易系统 - 回测")
print("=" * 60)
try:
run_full_backtest()
print("\n回测完成!")
except Exception as e:
import traceback
print(f"\n回测出错: {e}")
traceback.print_exc()
if __name__ == "__main__":
main()
+25
View File
@@ -0,0 +1,25 @@
"""策略模块 — 统一注册所有策略到 StrategyRegistry"""
from core.signal.registry import StrategyRegistry
from strategies.ma_cross import MACrossStrategy
from strategies.rsi import RSIStrategy
from strategies.bollinger import BollingerStrategy
from strategies.macd import MACDStrategy
from strategies.kdj import KDJStrategy
from strategies.turtle import TurtleStrategy
from strategies.mean_reversion import MeanReversionStrategy
from strategies.momentum_breakout import MomentumBreakoutStrategy
from strategies.daily_breakout import DailyBreakoutStrategy
from strategies.wave_theory import WaveTheoryStrategy
_registry = StrategyRegistry()
_registry.register('ma_cross', MACrossStrategy)
_registry.register('rsi', RSIStrategy)
_registry.register('bollinger', BollingerStrategy)
_registry.register('macd', MACDStrategy)
_registry.register('kdj', KDJStrategy)
_registry.register('turtle', TurtleStrategy)
_registry.register('mean_reversion', MeanReversionStrategy)
_registry.register('momentum_breakout', MomentumBreakoutStrategy)
_registry.register('daily_breakout', DailyBreakoutStrategy)
_registry.register('wave_theory', WaveTheoryStrategy)
+14 -5
View File
@@ -38,13 +38,22 @@ class DailyBreakoutStrategy(BaseStrategy):
def run_backtest(self, df):
df = df.copy()
df['time'] = pd.to_datetime(df['time'], unit='s')
# 'time' 可能是列(来自MT5原始数据)或索引(来自MultiTimeframeDataStore
if 'time' in df.columns:
df['time'] = pd.to_datetime(df['time'], unit='s')
elif isinstance(df.index, pd.DatetimeIndex):
df['time'] = df.index
else:
df['time'] = pd.to_datetime(df.index, unit='s')
df['date'] = df['time'].dt.date
daily_highs = df.groupby('date')['high'].transform('max')
daily_lows = df.groupby('date')['low'].transform('max')
# 用前一日的最高/最低价作为突破基准,避免未来数据泄露
daily_high = df.groupby('date')['high'].max()
daily_low = df.groupby('date')['low'].min()
prev_high = df['date'].map(daily_high.shift(1))
prev_low = df['date'].map(daily_low.shift(1))
signals = pd.Series(0, index=df.index)
signals[df['close'] > daily_highs.shift(1)] = 1
signals[df['close'] < daily_lows.shift(1)] = -1
signals[df['close'] > prev_high] = 1
signals[df['close'] < prev_low] = -1
return signals
+4 -1
View File
@@ -12,7 +12,10 @@ class KDJStrategy(BaseStrategy):
def _calculate_indicators(self, df):
low_min = df['low'].rolling(self.period).min()
high_max = df['high'].rolling(self.period).max()
rsv = (df['close'] - low_min) / (high_max - low_min) * 100
# 避免除零:当最高价==最低价时,RSV 取 50(中性)
price_range = high_max - low_min
rsv = (df['close'] - low_min) / price_range.replace(0, float('nan')) * 100
rsv = rsv.fillna(50)
df['k'] = rsv.ewm(com=2).mean()
df['d'] = df['k'].ewm(com=2).mean()
df['j'] = 3 * df['k'] - 2 * df['d']
+21 -8
View File
@@ -3,9 +3,10 @@ from .base_strategy import BaseStrategy
from config import STRATEGY_CONFIG
class MeanReversionStrategy(BaseStrategy):
"""均值回归策略 — 价格突破布林带后等待回归确认再入场(与 BollingerStrategy 的即时入场区分)"""
def __init__(self, data_provider, symbol, timeframe, period=None, std_dev=None):
super().__init__(data_provider, symbol, timeframe)
# 从配置中获取参数,如果传入参数则使用传入的参数
config = STRATEGY_CONFIG.get('mean_reversion', {})
self.period = period if period is not None else config.get('period', 20)
self.std_dev = std_dev if std_dev is not None else config.get('std_dev', 2.0)
@@ -19,21 +20,33 @@ class MeanReversionStrategy(BaseStrategy):
def generate_signal(self):
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 5)
if rates is None or len(rates) < self.period:
if rates is None or len(rates) < self.period + 1:
return 0
df = pd.DataFrame(rates)
df = self._calculate_indicators(df)
if df['close'].iloc[-1] > df['upper_band'].iloc[-1]:
return -1
elif df['close'].iloc[-1] < df['lower_band'].iloc[-1]:
# 回归确认:价格曾突破边界,现已回归内侧
prev_close = df['close'].iloc[-2]
prev_lower = df['lower_band'].iloc[-2]
prev_upper = df['upper_band'].iloc[-2]
curr_close = df['close'].iloc[-1]
curr_lower = df['lower_band'].iloc[-1]
curr_upper = df['upper_band'].iloc[-1]
# 买入:上一根K线跌破下轨,当前回升至下轨上方(回归确认)
if prev_close < prev_lower and curr_close >= curr_lower:
return 1
# 卖出:上一根K线突破上轨,当前回落至上轨下方(回归确认)
elif prev_close > prev_upper and curr_close <= curr_upper:
return -1
return 0
def run_backtest(self, df):
df = df.copy()
df = self._calculate_indicators(df)
signals = pd.Series(0, index=df.index)
signals[df['close'] > df['upper_band']] = -1
signals[df['close'] < df['lower_band']] = 1
return signals
# 前一根在轨外 + 当前回归轨内 = 买入
signals[(df['close'].shift(1) < df['lower_band'].shift(1)) & (df['close'] >= df['lower_band'])] = 1
# 前一根在轨外 + 当前回归轨内 = 卖出
signals[(df['close'].shift(1) > df['upper_band'].shift(1)) & (df['close'] <= df['upper_band'])] = -1
return signals
+20 -9
View File
@@ -3,27 +3,38 @@ from .base_strategy import BaseStrategy
from config import STRATEGY_CONFIG
class MomentumBreakoutStrategy(BaseStrategy):
def __init__(self, data_provider, symbol, timeframe, period=None):
"""动量突破策略 — 在通道突破基础上增加动量方向确认(与 TurtleStrategy 的纯突破区分)"""
def __init__(self, data_provider, symbol, timeframe, period=None, momentum_period=None):
super().__init__(data_provider, symbol, timeframe)
# 从配置中获取参数,如果传入参数则使用传入的参数
config = STRATEGY_CONFIG.get('momentum_breakout', {})
self.period = period if period is not None else config.get('period', 20)
self.momentum_period = momentum_period if momentum_period is not None else config.get('momentum_period', 10)
def _calculate_indicators(self, df):
df['high_period'] = df['high'].rolling(self.period).max()
df['low_period'] = df['low'].rolling(self.period).min()
# 计算动量:当前收盘价相对于 N 根前的涨跌幅
df['momentum'] = df['close'] - df['close'].shift(self.momentum_period)
return df
def generate_signal(self):
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 2)
if rates is None or len(rates) < self.period + 1:
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, max(self.period, self.momentum_period) + 5)
if rates is None or len(rates) < max(self.period, self.momentum_period) + 1:
return 0
df = pd.DataFrame(rates)
df = self._calculate_indicators(df)
if df['close'].iloc[-1] > df['high_period'].iloc[-2]:
close = df['close'].iloc[-1]
breakout_high = df['high_period'].iloc[-2]
breakout_low = df['low_period'].iloc[-2]
momentum = df['momentum'].iloc[-1]
# 突破上轨 + 正动量确认
if close > breakout_high and momentum > 0:
return 1
elif df['close'].iloc[-1] < df['low_period'].iloc[-2]:
# 跌破下轨 + 负动量确认
elif close < breakout_low and momentum < 0:
return -1
return 0
@@ -31,6 +42,6 @@ class MomentumBreakoutStrategy(BaseStrategy):
df = df.copy()
df = self._calculate_indicators(df)
signals = pd.Series(0, index=df.index)
signals[df['close'] > df['high_period'].shift(1)] = 1
signals[df['close'] < df['low_period'].shift(1)] = -1
return signals
signals[(df['close'] > df['high_period'].shift(1)) & (df['momentum'] > 0)] = 1
signals[(df['close'] < df['low_period'].shift(1)) & (df['momentum'] < 0)] = -1
return signals
+7 -4
View File
@@ -18,10 +18,13 @@ class RSIStrategy(BaseStrategy):
df = pd.DataFrame(rates)
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=self.period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=self.period).mean()
rs = gain / loss
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.ewm(com=self.period - 1, min_periods=self.period).mean()
avg_loss = loss.ewm(com=self.period - 1, min_periods=self.period).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
latest_rsi = rsi.iloc[-1]
+11 -8
View File
@@ -36,8 +36,9 @@ class WaveTheoryStrategy(BaseStrategy):
def _identify_wave_points(self, df):
window_size = self.wave_period * 2 + 1
df['local_high'] = df['high'].rolling(window=window_size, center=False).max().shift(-self.wave_period)
df['local_low'] = df['low'].rolling(window=window_size, center=False).min().shift(-self.wave_period)
# 纯后向窗口,不使用 shift 避免未来函数
df['local_high'] = df['high'].rolling(window=window_size, center=False).max()
df['local_low'] = df['low'].rolling(window=window_size, center=False).min()
wave_points = pd.Series(0, index=df.index)
wave_points[df['high'] == df['local_high']] = 1
wave_points[df['low'] == df['local_low']] = -1
@@ -109,12 +110,13 @@ class WaveTheoryStrategy(BaseStrategy):
if current_price > upper_bound * 0.98 and current_momentum < 0: return -1
elif current_price < lower_bound * 1.02 and current_momentum > 0: return 1
else:
ema_alignment = (df['ema_short'].iloc[-1] > df['ema_medium'].iloc[-1] > df['ema_long'].iloc[-1])
ema_bullish = (df['ema_short'].iloc[-1] > df['ema_medium'].iloc[-1] > df['ema_long'].iloc[-1])
ema_bearish = (df['ema_short'].iloc[-1] < df['ema_medium'].iloc[-1] < df['ema_long'].iloc[-1])
if f'fib_0.618' in df.columns and not pd.isna(df[f'fib_0.618'].iloc[-1]):
fib_618 = df[f'fib_0.618'].iloc[-1]
if abs(current_price - fib_618) / fib_618 < 0.01:
if ema_alignment and current_momentum > 0: return 1
elif not ema_alignment and current_momentum < 0: return -1
if ema_bullish and current_momentum > 0: return 1
elif ema_bearish and current_momentum < 0: return -1
return 0
def run_backtest(self, df):
@@ -132,10 +134,11 @@ class WaveTheoryStrategy(BaseStrategy):
if current_price > upper_bound * 0.98 and current_momentum < 0: signals.iat[i] = -1
elif current_price < lower_bound * 1.02 and current_momentum > 0: signals.iat[i] = 1
else:
ema_alignment = (df['ema_short'].iloc[i] > df['ema_medium'].iloc[i] > df['ema_long'].iloc[i])
ema_bullish = (df['ema_short'].iloc[i] > df['ema_medium'].iloc[i] > df['ema_long'].iloc[i])
ema_bearish = (df['ema_short'].iloc[i] < df['ema_medium'].iloc[i] < df['ema_long'].iloc[i])
if f'fib_0.618' in df.columns and not pd.isna(df[f'fib_0.618'].iloc[i]):
fib_618 = df[f'fib_0.618'].iloc[i]
if abs(current_price - fib_618) / fib_618 < 0.01:
if ema_alignment and current_momentum > 0: signals.iat[i] = 1
elif not ema_alignment and current_momentum < 0: signals.iat[i] = -1
if ema_bullish and current_momentum > 0: signals.iat[i] = 1
elif ema_bearish and current_momentum < 0: signals.iat[i] = -1
return signals
View File
+44
View File
@@ -0,0 +1,44 @@
# MT5 时间周期常量映射
# 参考: https://www.mql5.com/en/docs/constants/chartconstants/enum_timeframes
PERIOD_M1 = 1
PERIOD_M5 = 5
PERIOD_M15 = 15
PERIOD_M30 = 30
PERIOD_H1 = 16385
PERIOD_H4 = 16386
PERIOD_D1 = 16408
PERIOD_W1 = 32769
PERIOD_MN1 = 49153
# MT5 timeframe → 分钟数
TIMEFRAME_TO_MINUTES = {
PERIOD_M1: 1,
PERIOD_M5: 5,
PERIOD_M15: 15,
PERIOD_M30: 30,
PERIOD_H1: 60,
PERIOD_H4: 240,
PERIOD_D1: 1440,
PERIOD_W1: 10080,
PERIOD_MN1: 43200,
}
# MT5 timeframe → pandas resample rule
RESAMPLE_RULES = {
PERIOD_M1: '1min',
PERIOD_M5: '5min',
PERIOD_M15: '15min',
PERIOD_M30: '30min',
PERIOD_H1: '1h',
PERIOD_H4: '4h',
PERIOD_D1: '1D',
PERIOD_W1: '1W',
PERIOD_MN1: '1ME',
}
# MT5 交易方向常量
ORDER_TYPE_BUY = 0
ORDER_TYPE_SELL = 1
POSITION_TYPE_BUY = 0
POSITION_TYPE_SELL = 1