mirror of
https://github.com/silencesdg/mt5_python_ea_suite.git
synced 2026-07-27 18:57:44 +00:00
重构项目架构,新增 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:
@@ -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`)
|
||||
|
||||
@@ -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-10,5万根M1数据)
|
||||
SIGNAL_THRESHOLDS = {
|
||||
"buy_threshold": 1.5, # 买入信号阈值 (优化后)
|
||||
"sell_threshold": -0.87 # 卖出信号阈值 (优化后)
|
||||
"buy_threshold": 1.344,
|
||||
"sell_threshold": -2.980
|
||||
}
|
||||
|
||||
|
||||
# 风险管理参数
|
||||
# 风险管理参数(优化器结果 2026-05-10,5万根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-10,5万根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-10,5万根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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
"""平仓"""
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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: 回测传None(run_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
|
||||
Vendored
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
"""实时交易器 (已重构为依赖注入)"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -1,4 +1,7 @@
|
||||
MetaTrader5
|
||||
pandas
|
||||
deap
|
||||
tqdm
|
||||
tqdm
|
||||
fastapi
|
||||
uvicorn
|
||||
requests
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
@@ -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']
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user