mirror of
https://github.com/silencesdg/mt5_python_ea_suite.git
synced 2026-07-29 11:47:44 +00:00
基本完毕
This commit is contained in:
+6
-2
@@ -2,5 +2,9 @@
|
||||
/logs
|
||||
/myenv
|
||||
__pycache__/
|
||||
.json
|
||||
.csv
|
||||
*.json
|
||||
*.csv
|
||||
.claude/
|
||||
.claude/**
|
||||
.claude/settings.local.json
|
||||
.claude/
|
||||
@@ -1,60 +1,56 @@
|
||||
# MT5 Python EA Suite (MetaTrader 5 Python量化交易策略套件)
|
||||
# MT5 Python EA Suite (MetaTrader 5 量化交易策略套件)
|
||||
|
||||
这是一个基于Python与MetaTrader 5 (MT5)平台交互的量化交易策略套件。它提供了一个完整的框架,用于开发、回测、优化和运行由多个交易策略组成的投资组合。
|
||||
|
||||
系统的核心思想不是依赖于单一策略,而是将多个不同类型的策略(如趋势跟踪、均值回归、风险管理等)组合起来,通过加权投票和阈值过滤的机制,生成一个更稳健、更可靠的最终交易决策。
|
||||
这是一个基于Python与MetaTrader 5 (MT5)平台交互的、经过重构的现代化量化交易策略套件。它提供了一个清晰、可扩展的框架,用于开发、回测和运行由多个交易策略组成的投资组合。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
## 核心概念
|
||||
|
||||
- **多策略框架**: 支持同时运行任意数量的、不同逻辑的交易策略。
|
||||
- **可扩展的策略库**: 可以通过在`strategies`目录下添加新文件来轻松地创建和集成自定义策略。
|
||||
- **组合信号生成**: 将所有已激活策略的信号(买入/卖出/无操作)根据可配置的权重进行加权求和,并与买卖阈值进行比较,最终形成统一的交易指令。
|
||||
- **回测引擎**: 内置一个简单的回测引擎 (`backtest.py`),用于评估策略组合在历史数据上的表现。
|
||||
- **遗传算法优化器**: 集成了强大的遗传算法 (`optimizer.py`),能够自动学习和寻找能使历史收益最大化的最佳策略权重组合。
|
||||
- **实盘交易模式**: 提供了`run_realtime()`函数,用于连接正在运行的MT5客户端,根据组合信号生成实时的交易决策。
|
||||
- **状态管理**: 支持有状态的复杂策略,例如需要“记忆”当前持仓状态或趋势状态的策略(如盈利保护、容错趋势等)。
|
||||
- **多策略组合**: 系统的核心思想不是依赖于单一策略,而是将多个不同类型的策略(如趋势跟踪、均值回归等)组合起来,通过动态加权生成一个更稳健的交易决策。
|
||||
|
||||
- **动态权重**: 系统能根据对市场状态(如趋势、震荡)的分析,动态地调整不同策略的权重,在趋势行情中倚重趋势策略,在震荡行情中侧重于摆动策略。
|
||||
|
||||
- **依赖注入架构**: 项目采用依赖注入的设计模式,将核心交易逻辑与数据来源彻底分离。无论是处理实时行情、模拟数据还是历史数据,核心逻辑都保持不变,极大地保证了回测与实盘的一致性,并方便了未来的扩展。
|
||||
|
||||
- **配置即代码**: 所有的系统行为,从交易参数到策略组合,都通过 `config.py` 文件进行集中配置,清晰直观。
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
mt5_python_ea_suite/
|
||||
├── main.py # 项目主入口 (运行回测、优化或实盘)
|
||||
├── config.py # 全局配置文件 (策略、权重、阈值)
|
||||
├── optimizer.py # 遗传算法优化器
|
||||
├── backtest.py # 回测引擎
|
||||
├── utils.py # 工具函数 (MT5连接、订单处理等)
|
||||
├── logger.py # 日志配置
|
||||
├── requirements.txt # Python依赖包
|
||||
├── logs/ # 日志文件目录
|
||||
└── strategies/ # 核心:所有交易策略的存放目录
|
||||
├── ma_cross.py
|
||||
├── rsi.py
|
||||
├── profit_protect.py
|
||||
├── resilient_trend.py
|
||||
└── ... (其他策略)
|
||||
/ (根目录)
|
||||
├── core/ # 核心逻辑模块
|
||||
│ ├── __init__.py
|
||||
│ ├── data_providers.py # 数据提供者 (实盘/模拟/回测)
|
||||
│ ├── risk/ # 风险管理 (包含PositionManager)
|
||||
│ └── utils.py # 通用工具函数
|
||||
├── execution/ # 执行逻辑模块
|
||||
│ ├── __init__.py
|
||||
│ ├── backtest_engine.py # 回测引擎
|
||||
│ ├── dynamic_weights.py # 动态权重管理器
|
||||
│ └── realtime_trader.py # 实时交易处理器
|
||||
├── strategies/ # 策略库
|
||||
│ ├── __init__.py
|
||||
│ ├── base_strategy.py
|
||||
│ └── ... (具体策略)
|
||||
├── logs/ # 日志目录
|
||||
├── start_realtime.py # [入口] 启动实时/模拟交易
|
||||
├── start_backtest.py # [入口] 启动回测
|
||||
├── config.py # [核心] 全局配置文件
|
||||
├── requirements.txt # Python依赖
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 如何使用
|
||||
## 如何运行
|
||||
|
||||
#### 1. 环境准备
|
||||
|
||||
- 确保您的电脑上已经安装了MetaTrader 5交易终端。
|
||||
- 在MT5中,进入 `工具 > 选项 > EA交易`,确保勾选了“允许算法交易”和“允许DLL导入”。
|
||||
- 安装Python 3.x。
|
||||
- **创建并激活虚拟环境**:
|
||||
```bash
|
||||
python -m venv myenv
|
||||
# Windows
|
||||
myenv\Scripts\activate
|
||||
# macOS/Linux
|
||||
source myenv/bin/activate
|
||||
```
|
||||
- 安装Python 3.x并创建虚拟环境。
|
||||
- **安装依赖**:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
@@ -62,38 +58,42 @@ mt5_python_ea_suite/
|
||||
|
||||
#### 2. 核心配置 (`config.py`)
|
||||
|
||||
这是您与系统交互的主要文件。
|
||||
这是您与系统交互的唯一配置中心。在这里您可以调整几乎所有的参数,例如:
|
||||
- `INITIAL_CAPITAL`: 初始资金。
|
||||
- `USE_DATE_RANGE` / `BACKTEST_COUNT`: 选择回测模式(按日期或按K线数)。
|
||||
- `REALTIME_CONFIG`: **实盘/模拟交易的配置**,包括`dry_run`模式的开关。
|
||||
- `RISK_CONFIG`: 止盈、止损等风险参数。
|
||||
- `CAPITAL_ALLOCATION`: 多、空方向的资金分配比例。
|
||||
- `DEFAULT_WEIGHTS`: 策略的基础权重。
|
||||
|
||||
- **`STRATEGIES`**: 这是最重要的配置项。它是一个列表,每一项都是一个 `(策略实例, 权重)` 的元组。您可以在这里添加、删除策略,或者手动修改它们的权重。
|
||||
- **`BUY_THRESHOLD` / `SELL_THRESHOLD`**: 组合信号的触发阈值。所有策略的 `(信号 * 权重)` 之和,如果超过这个阈值,就会触发交易。
|
||||
#### 3. 运行回测
|
||||
|
||||
#### 3. 运行模式 (`main.py`)
|
||||
直接运行回测启动脚本。所有配置将从 `config.py` 读取。
|
||||
|
||||
打开`main.py`文件,在文件底部的 `if __name__ == "__main__"` 部分,您可以选择要执行的任务:
|
||||
```bash
|
||||
python start_backtest.py
|
||||
```
|
||||
回测结束后,会打印详细的性能报告,并将完整的交易记录保存到 `backtest_trades.csv` 和 `backtest_trades.json` 文件中。
|
||||
|
||||
- **运行回测 (`run_backtest()`)**:
|
||||
使用`config.py`中当前的权重和参数,对历史数据进行一次回测,并打印最终资金。
|
||||
#### 4. 运行实盘或模拟交易
|
||||
|
||||
- **运行优化 (`run_optimizer()`)**:
|
||||
**这是最推荐的步骤**。它会启动遗传算法,自动寻找`STRATEGIES`中每个策略的最佳权重。这个过程会非常耗时,因为它需要运行成百上千次回测。优化结束后,它会打印出找到的最佳权重组合。
|
||||
**重要**: 优化完成后,您需要**手动将找到的最佳权重复制回 `config.py` 文件**,以便在回测和实盘中应用。
|
||||
在 `config.py` 中,将 `REALTIME_CONFIG` 里的 `dry_run` 设置为 `False`(实盘)或 `True`(模拟),然后运行:
|
||||
|
||||
- **运行实盘 (`run_realtime()`)**:
|
||||
使用`config.py`中的权重,连接MT5,根据最新的市场数据生成实时的、组合后的交易信号。
|
||||
```bash
|
||||
python start_realtime.py
|
||||
```
|
||||
程序会根据您的配置,以实盘或模拟模式启动。
|
||||
|
||||
---
|
||||
|
||||
## 如何添加一个新策略
|
||||
|
||||
该框架的扩展性非常强。要添加一个您自己的新策略,只需三步:
|
||||
新架构下添加策略非常简单:
|
||||
|
||||
1. 在 `strategies/` 目录下,创建一个新的Python文件,例如 `my_new_strategy.py`。
|
||||
2. 在该文件中,创建一个名为 `Strategy` 的类,并至少实现两个核心方法:
|
||||
- `__init__(self)`: 初始化策略参数。
|
||||
- `run_backtest(self, df)`: 实现回测逻辑,需要返回一个包含 `1`(买), `-1`(卖), `0`(无操作) 的Pandas Series。
|
||||
- `generate_signal(self)`: 实现实盘信号生成逻辑,需要返回 `1`, `-1`, 或 `0`。
|
||||
3. 打开 `config.py` 文件:
|
||||
- 在文件顶部 `import` 您的新策略。
|
||||
- 在 `STRATEGIES` 列表中,添加一项 `(my_new_strategy.Strategy(), 1.0)`。
|
||||
1. 在 `strategies/` 目录下,创建一个新的Python文件,例如 `my_strategy.py`。
|
||||
2. 在该文件中,创建一个继承自 `base_strategy.BaseStrategy` 的策略类。
|
||||
3. 实现您的 `__init__` 和 `generate_signal` (用于实盘) 或 `run_backtest` (用于回测) 方法。
|
||||
4. 在 `execution/dynamic_weights.py` 文件的 `__init__` 方法中,将您的新策略添加到 `self.strategy_instances` 字典中。
|
||||
5. 在 `config.py` 的 `DEFAULT_WEIGHTS` 中,为您的新策略添加一个基础权重。
|
||||
|
||||
完成!您的新策略已经无缝集成到整个系统中,可以立即参与到回测和遗传算法优化中。
|
||||
完成!您的新策略已经无缝集成到整个系统中。
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
import pandas as pd
|
||||
from trade_logger import trade_logger
|
||||
from risk_management import RiskController
|
||||
from config import SIGNAL_THRESHOLDS
|
||||
from logger import logger
|
||||
class BacktestEngine:
|
||||
def __init__(self, df):
|
||||
"""
|
||||
df: 包含历史k线的DataFrame,至少包括open, high, low, close字段
|
||||
"""
|
||||
self.df = df
|
||||
|
||||
def run_strategy(self, strategy):
|
||||
"""
|
||||
执行策略的run_backtest,得到信号序列
|
||||
"""
|
||||
signals = strategy.run_backtest(self.df)
|
||||
# 记录策略信号统计
|
||||
buy_count = (signals == 1).sum()
|
||||
sell_count = (signals == -1).sum()
|
||||
neutral_count = (signals == 0).sum()
|
||||
logger.info(f"策略 {strategy.__class__.__module__} 信号统计 - 买入: {buy_count}, 卖出: {sell_count}, 中性: {neutral_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()
|
||||
neutral_signals = (combined_signal == 0).sum()
|
||||
logger.info(f"信号统计 - 买入: {buy_signals}, 卖出: {sell_signals}, 中性: {neutral_signals}")
|
||||
|
||||
return combined_signal
|
||||
|
||||
def calc_returns(self, signals):
|
||||
"""
|
||||
根据信号计算策略回测收益率(简化版)
|
||||
"""
|
||||
df = self.df.copy()
|
||||
df['signal'] = signals.shift(1).fillna(0) # 防止未来函数
|
||||
df['returns'] = df['close'].pct_change()
|
||||
df['strategy_returns'] = df['signal'] * df['returns']
|
||||
cum_ret = (1 + df['strategy_returns']).cumprod() - 1
|
||||
return cum_ret
|
||||
|
||||
def calc_returns_with_trades(self, signals, symbol="XAUUSD"):
|
||||
"""
|
||||
根据信号计算策略回测收益率,并记录每一笔交易
|
||||
使用RiskController进行风险管理,防止频繁交易
|
||||
"""
|
||||
df = self.df.copy()
|
||||
df['signal'] = signals.shift(1).fillna(0) # 防止未来函数
|
||||
|
||||
# 初始化风险管理器
|
||||
risk_controller = RiskController()
|
||||
|
||||
# 记录交易
|
||||
in_position = False
|
||||
position_direction = None
|
||||
position_price = 0
|
||||
position_time = None
|
||||
last_trade_time = None
|
||||
min_trade_interval = 0 # 取消最小交易间隔限制
|
||||
|
||||
for i in range(1, len(df)):
|
||||
current_signal = df['signal'].iloc[i]
|
||||
current_price = df['close'].iloc[i]
|
||||
current_time = df.index[i] if hasattr(df.index, 'to_pydatetime') else i
|
||||
|
||||
# 取消交易间隔限制检查
|
||||
can_trade = True
|
||||
|
||||
# 定期打印进度(每1000根K线)
|
||||
if i % 1000 == 0:
|
||||
logger.info(f"回测进度: {i}/{len(df)-1}")
|
||||
|
||||
# 开仓信号
|
||||
if current_signal != 0 and not in_position and can_trade:
|
||||
direction = 'buy' if current_signal == 1 else 'sell'
|
||||
|
||||
# 检查风险管理器是否允许交易
|
||||
if risk_controller.should_allow_trade(direction):
|
||||
trade_logger.open_position(symbol, direction, current_price, current_time, f"signal_{current_signal}")
|
||||
in_position = True
|
||||
position_direction = direction
|
||||
position_price = current_price
|
||||
position_time = current_time
|
||||
last_trade_time = current_time
|
||||
|
||||
# 更新RiskController的持仓信息
|
||||
position_type = "long" if direction == 'buy' else "short"
|
||||
risk_controller.update_position_entry(current_price, position_type)
|
||||
|
||||
# 持仓时检查风险管理条件(无论信号如何)
|
||||
if in_position:
|
||||
# 使用RiskController检查风险管理条件
|
||||
risk_action, risk_reason = risk_controller.check_risk_management(current_price)
|
||||
|
||||
if risk_action != "none":
|
||||
trade_logger.close_position(symbol, current_price, current_time, f"risk_management_{risk_action}")
|
||||
risk_controller.execute_risk_action(risk_action, risk_reason)
|
||||
in_position = False
|
||||
continue # 跳过后续信号处理
|
||||
|
||||
# 中性信号处理
|
||||
elif current_signal == 0 and in_position:
|
||||
trade_logger.close_position(symbol, current_price, current_time, "signal_to_neutral")
|
||||
in_position = False
|
||||
|
||||
# 反向信号(取消交易间隔限制)
|
||||
elif current_signal != 0 and in_position and can_trade:
|
||||
# 只有当信号明显反向时才平仓并反向
|
||||
if (current_signal == 1 and position_direction == 'sell') or \
|
||||
(current_signal == -1 and position_direction == 'buy'):
|
||||
# 先平仓
|
||||
trade_logger.close_position(symbol, current_price, current_time, "signal_reverse")
|
||||
in_position = False
|
||||
|
||||
# 立即开新仓(取消等待间隔)
|
||||
direction = 'buy' if current_signal == 1 else 'sell'
|
||||
if risk_controller.should_allow_trade(direction):
|
||||
trade_logger.open_position(symbol, direction, current_price, current_time, f"signal_{current_signal}")
|
||||
in_position = True
|
||||
position_direction = direction
|
||||
position_price = current_price
|
||||
position_time = current_time
|
||||
last_trade_time = current_time
|
||||
|
||||
# 更新RiskController的持仓信息
|
||||
position_type = "long" if direction == 'buy' else "short"
|
||||
risk_controller.update_position_entry(current_price, position_type)
|
||||
|
||||
# 持仓到最后
|
||||
elif in_position and i == len(df) - 1:
|
||||
trade_logger.close_position(symbol, current_price, current_time, "end_of_backtest")
|
||||
in_position = False
|
||||
|
||||
# 计算收益率
|
||||
df['returns'] = df['close'].pct_change()
|
||||
df['strategy_returns'] = df['signal'] * df['returns']
|
||||
cum_ret = (1 + df['strategy_returns']).cumprod() - 1
|
||||
return cum_ret
|
||||
@@ -1,10 +1,11 @@
|
||||
# 交易配置
|
||||
SYMBOL = "XAUUSD"
|
||||
INTERVAL = 60 # 秒
|
||||
INITIAL_CAPITAL = 10000 # 初始资金
|
||||
INITIAL_CAPITAL = 20000 # 初始资金
|
||||
SPREAD = 32 # 点差(点数),买卖合起来的总共点差成本
|
||||
|
||||
# 时间配置
|
||||
TIMEFRAME = 1 # M1 (1分钟图)
|
||||
TIMEFRAME = 1# M1 (1分钟图) - MT5常量值
|
||||
|
||||
# 回测时间范围 (格式: "YYYY-MM-DD")
|
||||
# 注意:这些日期需要确保在MT5服务器上有可用数据
|
||||
@@ -21,73 +22,258 @@ USE_DATE_RANGE = False # 设置为False可强制使用数据量模式
|
||||
|
||||
# 兼容性配置 (如果日期配置不可用,则使用数据量)
|
||||
BACKTEST_COUNT = 30000 # 回测数据量
|
||||
OPTIMIZER_COUNT = 5000 # 优化器数据量
|
||||
OPTIMIZER_COUNT = 15000 # 优化器数据量
|
||||
|
||||
RISK_CONFIG_CONST = {
|
||||
'enable_time_based_exit': True
|
||||
}
|
||||
|
||||
# 资金分配配置
|
||||
CAPITAL_ALLOCATION = {
|
||||
"long_pct": 0.7, # 多头持仓分配资金比例
|
||||
"short_pct": 0.3, # 空头持仓分配资金比例
|
||||
}
|
||||
|
||||
# 模拟交易特定配置 (用于dry_run模式)
|
||||
SIMULATION_CONFIG = {
|
||||
"leverage": 100, # 模拟杠杆
|
||||
"contract_size": 1, # XAUUSD的合约大小
|
||||
"volume_step": 0.01, # 交易手数步长
|
||||
"volume_min": 0.01, # 最小交易手数
|
||||
"volume_max": 100.0, # 最大交易手数
|
||||
"spread": 16, # 点差(点数)
|
||||
}
|
||||
|
||||
# 回测配置
|
||||
BACKTEST_CONFIG = {
|
||||
"trade_direction": "both", # 交易方向: "long"(只做多), "short"(只做空), "both"(多空都支持)
|
||||
"spread": 16, # 点差(点数)
|
||||
}
|
||||
|
||||
|
||||
# 实时交易配置
|
||||
REALTIME_CONFIG = {
|
||||
"update_interval": 5, # 更新间隔(秒)
|
||||
"daily_reset_time": "00:00", # 每日重置时间
|
||||
"max_long_positions": 3, # 最大多头持仓数(增加为3个)
|
||||
"max_short_positions": 3, # 最大空头持仓数(增加为3个)
|
||||
"min_trade_interval": 0, # 最小交易间隔(分钟),0表示无限制
|
||||
"enable_auto_trading": True, # 是否启用自动交易
|
||||
"dry_run": False, # 是否为模拟运行(不实际下单)
|
||||
"logging_level": "DEBUG", # 日志级别
|
||||
"trade_direction": "both", # 交易方向: "long"(只做多), "short"(只做空), "both"(多空都支持)
|
||||
}
|
||||
|
||||
|
||||
# 数据获取配置
|
||||
DATA_CONFIG = {
|
||||
"m1_bars_count": 5000, # 1分钟K线数据获取数量
|
||||
}
|
||||
|
||||
|
||||
# 遗传算法优化器配置
|
||||
GENETIC_OPTIMIZER_CONFIG = {
|
||||
# 算法参数
|
||||
"population_size": 200, # 种群大小
|
||||
"generations": 50, # 进化代数
|
||||
"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, # 保存代数信息
|
||||
}
|
||||
|
||||
|
||||
'''
|
||||
此处上面的是固定的参数,可手动调整
|
||||
--------------------------------------------------------
|
||||
此处下面所有参数,都将进入优化器进行优化
|
||||
'''
|
||||
|
||||
# 信号阈值配置
|
||||
SIGNAL_THRESHOLDS = {
|
||||
"buy_threshold": 0.5, # 买入信号阈值
|
||||
"sell_threshold": -0.5, # 卖出信号阈值
|
||||
"buy_threshold": 1.5, # 买入信号阈值 (优化后)
|
||||
"sell_threshold": -0.87 # 卖出信号阈值 (优化后)
|
||||
}
|
||||
|
||||
|
||||
# 风险管理参数
|
||||
RISK_CONFIG = {
|
||||
"stop_loss_pct": -0.001, # 固定止损:亏损0.1%
|
||||
"profit_retracement_pct": 0.10, # 利润回撤10%止盈
|
||||
"min_profit_for_trailing": 0.05, # 追踪止损激活阈值:利润0.5%
|
||||
"take_profit_pct": 0.20, # 固定止盈:盈利20%
|
||||
"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.10, # 最大日亏损10%
|
||||
"max_daily_loss": -0.3, # 最大日亏损30%
|
||||
"max_holding_minutes": 140, # 持仓超过140分钟 (优化后)
|
||||
"min_profit_for_time_exit": 0.01, # 且盈利未达到0.1%则平仓 (优化后)
|
||||
}
|
||||
|
||||
# 市场状态分析参数
|
||||
MARKET_STATE_CONFIG = {
|
||||
"trend_period": 50,
|
||||
"retracement_tolerance": 0.30,
|
||||
"volume_period": 20,
|
||||
"volume_ma_period": 10,
|
||||
"trend_period": 44,
|
||||
"retracement_tolerance": 0.1382775008306345,
|
||||
"volume_period": 13,
|
||||
"volume_ma_period": 12,
|
||||
}
|
||||
|
||||
# 波浪理论策略配置
|
||||
WAVE_THEORY_CONFIG = {
|
||||
"daily_data_count": 30, # 日线数据天数
|
||||
"ema_short": 5,
|
||||
"ema_medium": 13,
|
||||
"ema_long": 34,
|
||||
"wave_period": 21,
|
||||
"range_period": 20,
|
||||
"adx_period": 14,
|
||||
# 策略参数配置 (从优化器中提取的优化参数)
|
||||
STRATEGY_CONFIG = {
|
||||
# MACrossStrategy 参数
|
||||
"ma_cross": {
|
||||
"short_window": 5,
|
||||
"long_window": 39,
|
||||
},
|
||||
|
||||
# RSIStrategy 参数
|
||||
"rsi": {
|
||||
"period": 22,
|
||||
"overbought": 80,
|
||||
"oversold": 24,
|
||||
},
|
||||
|
||||
# BollingerStrategy 参数
|
||||
"bollinger": {
|
||||
"period": 10,
|
||||
"std_dev": 2.8916513144581044,
|
||||
},
|
||||
|
||||
# MACDStrategy 参数
|
||||
"macd": {
|
||||
"fast_ema": 17,
|
||||
"slow_ema": 22,
|
||||
"signal_period": 13,
|
||||
},
|
||||
|
||||
# MeanReversionStrategy 参数
|
||||
"mean_reversion": {
|
||||
"period": 40,
|
||||
"std_dev": 2.917216289407233,
|
||||
},
|
||||
|
||||
# MomentumBreakoutStrategy 参数
|
||||
"momentum_breakout": {
|
||||
"period": 27,
|
||||
},
|
||||
|
||||
# KDJStrategy 参数
|
||||
"kdj": {
|
||||
"period": 6,
|
||||
},
|
||||
|
||||
# TurtleStrategy 参数
|
||||
"turtle": {
|
||||
"period": 16,
|
||||
},
|
||||
|
||||
# DailyBreakoutStrategy 参数
|
||||
"daily_breakout": {
|
||||
"bars_count": 807,
|
||||
},
|
||||
|
||||
# WaveTheoryStrategy 参数
|
||||
"wave_theory": {
|
||||
"ema_short": 3,
|
||||
"ema_medium": 15,
|
||||
"ema_long": 36,
|
||||
"wave_period": 28,
|
||||
"range_period": 30,
|
||||
"adx_period": 21,
|
||||
"momentum_period": 13,
|
||||
"range_threshold": -0.02028040971155598,
|
||||
"adx_threshold": 22,
|
||||
},
|
||||
}
|
||||
|
||||
# 市场趋势判断权重配置
|
||||
TREND_INDICATOR_WEIGHTS = {
|
||||
"price_breakout": 0.35, # 价格突破权重
|
||||
"volume_confirmation": 0.25, # 成交量确认权重
|
||||
"momentum oscillator": 0.20, # 动量震荡指标权重
|
||||
"moving_average": 0.20, # 移动平均线权重
|
||||
"price_breakout": 0.03552729699860058, # 价格突破权重
|
||||
"volume_confirmation": 0.2564191814903339, # 成交量确认权重
|
||||
"momentum oscillator": 0.48690789892001296, # 动量震荡指标权重
|
||||
"moving_average": 0.3549274308680269, # 移动平均线权重
|
||||
}
|
||||
|
||||
# 趋势判断阈值
|
||||
TREND_THRESHOLDS = {
|
||||
"strong_trend": 0.7, # 强趋势阈值
|
||||
"weak_trend": 0.4, # 弱趋势阈值
|
||||
"volume_spike": 1.5, # 成交量突增倍数
|
||||
"oversold": 30, # 超卖阈值 (RSI)
|
||||
"overbought": 70, # 超买阈值 (RSI)
|
||||
"strong_trend": 0.24063407379490956, # 强趋势阈值
|
||||
"weak_trend": 0.3708545035763192, # 弱趋势阈值
|
||||
"volume_spike": 1.7781348694952452, # 成交量突增倍数
|
||||
"oversold": 29, # 超卖阈值 (RSI)
|
||||
"overbought": 69, # 超买阈值 (RSI)
|
||||
}
|
||||
|
||||
# 动态权重配置(当市场状态判断置信度低时使用的基础权重)
|
||||
|
||||
# 动态权重配置(经过优化器优化的最佳权重)
|
||||
DEFAULT_WEIGHTS = {
|
||||
"ma_cross": 1.31,
|
||||
"rsi": 0.41,
|
||||
"bollinger": 1.19,
|
||||
"mean_reversion": 0.75,
|
||||
"momentum_breakout": 1.5,
|
||||
"macd": 0.8,
|
||||
"kdj": 2.02,
|
||||
"turtle": 0.11,
|
||||
"daily_breakout": 0.28,
|
||||
"wave_theory": 0.8, # 波浪理论策略基础权重
|
||||
"risk_management": 2.0, # 风险管理策略权重
|
||||
"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,
|
||||
}
|
||||
|
||||
# 市场状态策略权重配置
|
||||
MARKET_STATE_WEIGHTS = {
|
||||
"uptrend": {
|
||||
"ma_cross": 1.50,
|
||||
"momentum_breakout": 1.20,
|
||||
"turtle": 0.25,
|
||||
"macd": 0.35,
|
||||
"daily_breakout": 1.50,
|
||||
"rsi": 1.00,
|
||||
"bollinger": 1.00,
|
||||
"kdj": 0.40,
|
||||
"mean_reversion": 0.80,
|
||||
"wave_theory": 0.20
|
||||
},
|
||||
"downtrend": {
|
||||
"ma_cross": 1.50,
|
||||
"momentum_breakout": 1.20,
|
||||
"turtle": 0.25,
|
||||
"macd": 0.35,
|
||||
"daily_breakout": 1.50,
|
||||
"rsi": 1.00,
|
||||
"bollinger": 1.00,
|
||||
"kdj": 0.40,
|
||||
"mean_reversion": 0.80,
|
||||
"wave_theory": 0.20
|
||||
},
|
||||
"ranging": {
|
||||
"rsi": 1.60,
|
||||
"bollinger": 1.70,
|
||||
"mean_reversion": 1.50,
|
||||
"kdj": 1.00,
|
||||
"wave_theory": 0.50,
|
||||
"ma_cross": 0.70,
|
||||
"macd": 0.20,
|
||||
"turtle": 0.10,
|
||||
"momentum_breakout": 0.50,
|
||||
"daily_breakout": 0.90
|
||||
},
|
||||
"none": DEFAULT_WEIGHTS
|
||||
}
|
||||
|
||||
# 市场趋势置信度阈值配置
|
||||
CONFIDENCE_THRESHOLDS = {
|
||||
"high_confidence": 0.941049792261965, # 高置信度阈值
|
||||
"medium_confidence": 0.8881796011658835, # 中等置信度阈值
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
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
|
||||
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
@@ -0,0 +1,151 @@
|
||||
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
|
||||
|
||||
class MarketStateAnalyzer:
|
||||
"""
|
||||
市场状态分析器 (支持参数优化)
|
||||
"""
|
||||
|
||||
def __init__(self, data_provider, market_state_params=None, trend_weights=None, trend_thresholds=None, confidence_thresholds=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.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')
|
||||
|
||||
def _calculate_volume_indicators(self, df):
|
||||
df['volume'] = df['tick_volume']
|
||||
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
|
||||
|
||||
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
|
||||
df['rsi'] = 100 - (100 / (1 + rs))
|
||||
exp1 = df['close'].ewm(span=12).mean()
|
||||
exp2 = df['close'].ewm(span=26).mean()
|
||||
df['macd'] = exp1 - exp2
|
||||
df['macd_signal'] = df['macd'].ewm(span=9).mean()
|
||||
df['macd_histogram'] = df['macd'] - df['macd_signal']
|
||||
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]
|
||||
price_range = high_period - low_period
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
def _update_trend_state(self, df, new_state):
|
||||
current_price = df['close'].iloc[-1]
|
||||
if new_state == "uptrend":
|
||||
self.current_trend = "uptrend"
|
||||
self.trend_peak = max(self.trend_peak, current_price)
|
||||
elif new_state == "downtrend":
|
||||
self.current_trend = "downtrend"
|
||||
self.trend_trough = min(self.trend_trough, current_price)
|
||||
else:
|
||||
if self.current_trend == "uptrend" and current_price < self.trend_peak * (1 - self.retracement_tolerance):
|
||||
self.current_trend = "none"
|
||||
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)
|
||||
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
|
||||
|
||||
self._update_trend_state(df, state)
|
||||
return state, confidence
|
||||
|
||||
def get_strategy_weights(self, market_state, confidence):
|
||||
base_weights = MARKET_STATE_WEIGHTS.get(market_state, DEFAULT_WEIGHTS)
|
||||
|
||||
high_confidence_threshold = self.confidence_thresholds.get("high_confidence", 0.7)
|
||||
medium_confidence_threshold = self.confidence_thresholds.get("medium_confidence", 0.4)
|
||||
|
||||
if confidence > high_confidence_threshold:
|
||||
return {k: v * confidence for k, v in base_weights.items()}
|
||||
elif confidence > medium_confidence_threshold:
|
||||
return {k: (v * confidence + DEFAULT_WEIGHTS.get(k, 1.0) * (1 - confidence)) for k, v in base_weights.items()}
|
||||
else:
|
||||
return DEFAULT_WEIGHTS
|
||||
@@ -0,0 +1,429 @@
|
||||
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}")
|
||||
@@ -22,6 +22,24 @@ def get_rates(symbol, timeframe, count, start_date=None, end_date=None):
|
||||
- start_date: 开始日期 (格式: "YYYY-MM-DD" 或 datetime对象)
|
||||
- end_date: 结束日期 (格式: "YYYY-MM-DD" 或 datetime对象)
|
||||
"""
|
||||
# 检查MT5连接状态
|
||||
if not mt5.terminal_info():
|
||||
logger.warning("MT5终端未连接,尝试重新连接...")
|
||||
if not initialize():
|
||||
logger.error("MT5重新连接失败")
|
||||
return None
|
||||
|
||||
# 检查交易品种是否可用
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
logger.error(f"交易品种 {symbol} 不可用")
|
||||
return None
|
||||
|
||||
if not symbol_info.visible:
|
||||
logger.info(f"交易品种 {symbol} 不可见,尝试启用...")
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
logger.error(f"无法启用交易品种 {symbol}")
|
||||
return None
|
||||
if start_date is not None and end_date is not None:
|
||||
# 使用日期范围获取数据
|
||||
import datetime
|
||||
@@ -44,7 +62,7 @@ def get_rates(symbol, timeframe, count, start_date=None, end_date=None):
|
||||
if rates is None:
|
||||
logger.info(f"获取{symbol}从{start_date.date()}到{end_date.date()}的历史数据失败")
|
||||
return None
|
||||
logger.info(f"成功获取{symbol}从{start_date.date()}到{end_date.date()}的历史数据,共{len(rates)}条")
|
||||
logger.debug(f"成功获取{symbol}从{start_date.date()}到{end_date.date()}的历史数据,共{len(rates)}条")
|
||||
except Exception as e:
|
||||
logger.error(f"使用日期范围获取数据失败: {e}")
|
||||
logger.info("回退到使用数据量获取数据")
|
||||
@@ -52,14 +70,14 @@ def get_rates(symbol, timeframe, count, start_date=None, end_date=None):
|
||||
if rates is None:
|
||||
logger.info(f"获取{symbol}历史数据失败")
|
||||
return None
|
||||
logger.info(f"成功获取{symbol}历史数据,共{len(rates)}条")
|
||||
logger.debug(f"成功获取{symbol}历史数据,共{len(rates)}条")
|
||||
else:
|
||||
# 使用数据量获取数据
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, count)
|
||||
if rates is None:
|
||||
logger.info(f"获取{symbol}历史数据失败")
|
||||
return None
|
||||
logger.info(f"成功获取{symbol}历史数据,共{len(rates)}条")
|
||||
logger.debug(f"成功获取{symbol}历史数据,共{len(rates)}条")
|
||||
|
||||
return rates
|
||||
|
||||
@@ -90,7 +108,7 @@ def send_order(symbol, order_type, volume=0.01):
|
||||
symbol_info_tick = mt5.symbol_info_tick(symbol)
|
||||
if symbol_info_tick is None:
|
||||
logger.error(f"无法获取{symbol}行情")
|
||||
return
|
||||
return None
|
||||
|
||||
price = symbol_info_tick.ask if order_type == "buy" else symbol_info_tick.bid
|
||||
order_type_mt5 = mt5.ORDER_TYPE_BUY if order_type == "buy" else mt5.ORDER_TYPE_SELL
|
||||
@@ -110,14 +128,75 @@ def send_order(symbol, order_type, volume=0.01):
|
||||
result = mt5.order_send(request)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
logger.error(f"下单失败,retcode={result.retcode}")
|
||||
return None
|
||||
else:
|
||||
logger.info(f"下单成功: {order_type} {symbol} {volume}")
|
||||
logger.info(f"下单成功: {order_type} {symbol} {volume}, ticket: {result.order}")
|
||||
return result
|
||||
|
||||
def close_position(ticket, symbol, volume):
|
||||
"""根据ticket平掉一个特定的仓位"""
|
||||
# In MT5, you close a position by creating an opposite order.
|
||||
# We need to get the position details first.
|
||||
positions = mt5.positions_get(ticket=ticket)
|
||||
if not positions:
|
||||
logger.error(f"无法找到ticket为 {ticket} 的持仓")
|
||||
return False
|
||||
|
||||
pos = positions[0] # positions_get returns a tuple of objects
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"position": pos.ticket,
|
||||
"symbol": symbol,
|
||||
"volume": volume,
|
||||
"type": mt5.ORDER_TYPE_SELL if pos.type == 0 else mt5.ORDER_TYPE_BUY, # pos.type == 0 is a BUY position
|
||||
"price": mt5.symbol_info_tick(symbol).bid if pos.type == 0 else mt5.symbol_info_tick(symbol).ask,
|
||||
"deviation": 20,
|
||||
"magic": 234000,
|
||||
"comment": f"Close position {ticket}",
|
||||
"type_filling": mt5.ORDER_FILLING_RETURN,
|
||||
}
|
||||
result = mt5.order_send(request)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
logger.error(f"平仓失败 ticket {ticket}, retcode={result.retcode}")
|
||||
return False
|
||||
else:
|
||||
logger.info(f"平仓成功 ticket {ticket}")
|
||||
return True
|
||||
|
||||
def get_current_price(symbol):
|
||||
"""获取当前价格"""
|
||||
try:
|
||||
# 检查MT5连接状态
|
||||
if not mt5.terminal_info():
|
||||
logger.warning("MT5终端未连接,尝试重新连接...")
|
||||
if not initialize():
|
||||
logger.error("MT5重新连接失败")
|
||||
return None
|
||||
|
||||
# 检查交易品种是否可用
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
logger.error(f"交易品种 {symbol} 不可用")
|
||||
return None
|
||||
|
||||
if not symbol_info.visible:
|
||||
logger.info(f"交易品种 {symbol} 不可见,尝试启用...")
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
logger.error(f"无法启用交易品种 {symbol}")
|
||||
return None
|
||||
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
return tick.bid if tick else None
|
||||
if tick is None:
|
||||
logger.error(f"无法获取 {symbol} 的价格信息")
|
||||
return None
|
||||
|
||||
return {
|
||||
'bid': tick.bid,
|
||||
'ask': tick.ask,
|
||||
'last': tick.last,
|
||||
'time': pd.to_datetime(tick.time, unit='s')
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取当前价格失败: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,97 @@
|
||||
import MetaTrader5 as mt5
|
||||
|
||||
# 登录账号
|
||||
ACCOUNT = 12345678
|
||||
PASSWORD = "your_password"
|
||||
SERVER = "Exness-MT5Trial" # 服务器名称
|
||||
SYMBOL = "EURUSD"
|
||||
|
||||
# 初始化
|
||||
if not mt5.initialize():
|
||||
print("initialize() failed:", mt5.last_error())
|
||||
quit()
|
||||
|
||||
if not mt5.login(ACCOUNT, PASSWORD, SERVER):
|
||||
print("login() failed:", mt5.last_error())
|
||||
mt5.shutdown()
|
||||
quit()
|
||||
|
||||
# 获取当前持仓
|
||||
positions = mt5.positions_get(symbol=SYMBOL)
|
||||
if positions is None or len(positions) == 0:
|
||||
print(f"没有 {SYMBOL} 的持仓")
|
||||
mt5.shutdown()
|
||||
quit()
|
||||
|
||||
# 遍历并平仓
|
||||
for pos in positions:
|
||||
ticket = pos.ticket
|
||||
lot = pos.volume
|
||||
position_type = pos.type # 0=BUY, 1=SELL
|
||||
|
||||
if position_type == mt5.POSITION_TYPE_BUY:
|
||||
# 平多 -> 发送SELL订单
|
||||
close_request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": SYMBOL,
|
||||
"volume": lot,
|
||||
"type": mt5.ORDER_TYPE_SELL,
|
||||
"position": ticket,
|
||||
"price": mt5.symbol_info_tick(SYMBOL).bid,
|
||||
"deviation": 10,
|
||||
"magic": 234000,
|
||||
"comment": "Python Close Buy",
|
||||
"type_filling": mt5.ORDER_FILLING_IOC,
|
||||
}
|
||||
else:
|
||||
# 平空 -> 发送BUY订单
|
||||
close_request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": SYMBOL,
|
||||
"volume": lot,
|
||||
"type": mt5.ORDER_TYPE_BUY,
|
||||
"position": ticket,
|
||||
"price": mt5.symbol_info_tick(SYMBOL).ask,
|
||||
"deviation": 10,
|
||||
"magic": 234000,
|
||||
"comment": "Python Close Sell",
|
||||
"type_filling": mt5.ORDER_FILLING_IOC,
|
||||
}
|
||||
|
||||
result = mt5.order_send(close_request)
|
||||
print(result)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
print(f"平仓失败: {result.comment}, 错误码: {result.retcode}")
|
||||
|
||||
mt5.shutdown()
|
||||
|
||||
'''
|
||||
2️⃣ 常见平仓失败原因
|
||||
未传 position 参数
|
||||
|
||||
在 MT5 里,如果是平仓,你必须指定 原单的 ticket ID(即 position 字段)。
|
||||
|
||||
不然系统会认为你是开新仓,而不是平仓。
|
||||
|
||||
价格方向不对
|
||||
|
||||
平多必须用 SELL,平空必须用 BUY,价格要对应 bid 或 ask。
|
||||
|
||||
多单平仓价用 Bid(卖给市场),空单平仓价用 Ask(买回)。
|
||||
|
||||
lot 不匹配
|
||||
|
||||
发送的 volume 必须和剩余仓位量一致(或等于合约最小手数的整数倍)。
|
||||
|
||||
有些券商要求精确到 2~3 位小数,否则 Invalid volume。
|
||||
|
||||
订单类型与服务器支持不符
|
||||
|
||||
type_filling 要用服务器支持的方式(常见 ORDER_FILLING_IOC)。
|
||||
|
||||
有些外汇服务器不支持 FOK。
|
||||
|
||||
symbol 没有准备好
|
||||
|
||||
必须 mt5.symbol_select(SYMBOL, True),并且 symbol_info_tick() 不为 None。
|
||||
'''
|
||||
@@ -1,90 +0,0 @@
|
||||
from strategies import ma_cross, rsi, bollinger, mean_reversion, momentum_breakout, macd, kdj, turtle, daily_breakout, wave_theory, risk_management
|
||||
from config import DEFAULT_WEIGHTS
|
||||
from logger import logger
|
||||
class DynamicWeightManager:
|
||||
"""
|
||||
动态权重管理器,根据风险管理器的建议动态调整策略权重
|
||||
"""
|
||||
|
||||
def __init__(self, risk_controller):
|
||||
self.risk_controller = risk_controller
|
||||
|
||||
# 策略实例映射
|
||||
self.strategy_instances = {
|
||||
'ma_cross': ma_cross.Strategy(),
|
||||
'rsi': rsi.Strategy(),
|
||||
'bollinger': bollinger.Strategy(),
|
||||
'mean_reversion': mean_reversion.Strategy(),
|
||||
'momentum_breakout': momentum_breakout.Strategy(),
|
||||
'macd': macd.Strategy(),
|
||||
'kdj': kdj.Strategy(),
|
||||
'turtle': turtle.Strategy(),
|
||||
'daily_breakout': daily_breakout.Strategy(),
|
||||
'wave_theory': wave_theory.Strategy(),
|
||||
'risk_management': risk_management.Strategy()
|
||||
}
|
||||
|
||||
# 当前权重配置
|
||||
self.current_weights = None
|
||||
self.current_market_state = "none"
|
||||
self.current_confidence = 0.0
|
||||
|
||||
def get_current_strategies_and_weights(self):
|
||||
"""
|
||||
获取当前策略实例和权重配置
|
||||
返回: [(strategy_instance, weight), ...]
|
||||
"""
|
||||
# 获取动态权重
|
||||
dynamic_weights, market_state, confidence = self.risk_controller.get_dynamic_weights()
|
||||
|
||||
# 更新当前状态
|
||||
self.current_weights = dynamic_weights
|
||||
self.current_market_state = market_state
|
||||
self.current_confidence = confidence
|
||||
|
||||
# 构建策略列表
|
||||
strategies_with_weights = []
|
||||
for strategy_name, weight in dynamic_weights.items():
|
||||
if strategy_name in self.strategy_instances:
|
||||
strategies_with_weights.append(
|
||||
(self.strategy_instances[strategy_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
|
||||
}
|
||||
|
||||
def get_strategy_instance(self, strategy_name):
|
||||
"""
|
||||
获取指定策略的实例
|
||||
"""
|
||||
return self.strategy_instances.get(strategy_name)
|
||||
|
||||
def add_custom_strategy(self, strategy_name, strategy_instance, base_weight=1.0):
|
||||
"""
|
||||
添加自定义策略
|
||||
"""
|
||||
self.strategy_instances[strategy_name] = strategy_instance
|
||||
logger.info(f"添加自定义策略: {strategy_name}, 基础权重: {base_weight}")
|
||||
|
||||
def remove_strategy(self, strategy_name):
|
||||
"""
|
||||
移除策略
|
||||
"""
|
||||
if strategy_name in self.strategy_instances:
|
||||
del self.strategy_instances[strategy_name]
|
||||
logger.info(f"移除策略: {strategy_name}")
|
||||
|
||||
def list_available_strategies(self):
|
||||
"""
|
||||
列出所有可用策略
|
||||
"""
|
||||
return list(self.strategy_instances.keys())
|
||||
@@ -0,0 +1,95 @@
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
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,141 @@
|
||||
import time
|
||||
import signal
|
||||
import sys
|
||||
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
|
||||
|
||||
class RealtimeTrader:
|
||||
"""实时交易器 (已重构为依赖注入)"""
|
||||
|
||||
def __init__(self, data_provider, update_interval=60):
|
||||
self.data_provider = data_provider
|
||||
self.update_interval = update_interval
|
||||
self.running = False
|
||||
self.risk_controller = None
|
||||
self.weight_manager = None
|
||||
|
||||
def _initialize(self):
|
||||
if not self.data_provider.initialize():
|
||||
return False
|
||||
|
||||
self.risk_controller = RiskController(self.data_provider)
|
||||
self.weight_manager = DynamicWeightManager(self.data_provider)
|
||||
|
||||
self.risk_controller.sync_state()
|
||||
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
logger.info("实时交易系统初始化完成")
|
||||
return True
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
logger.info(f"接收到信号 {signum},准备退出...")
|
||||
self.stop()
|
||||
|
||||
def _run_cycle(self):
|
||||
try:
|
||||
self.risk_controller.sync_state()
|
||||
|
||||
current_price = self.data_provider.get_current_price(SYMBOL)
|
||||
if not current_price:
|
||||
logger.warning("无法获取当前价格,跳过本次循环")
|
||||
return
|
||||
|
||||
strategies_with_weights = self.weight_manager.get_current_strategies_and_weights()
|
||||
if not strategies_with_weights: return
|
||||
|
||||
signals, weights = [], []
|
||||
for strat, weight in strategies_with_weights:
|
||||
signals.append(strat.generate_signal())
|
||||
weights.append(weight)
|
||||
|
||||
# 打印详细信号日志
|
||||
logger.info("--- 信号计算详情 ---")
|
||||
for i, (strat, weight) in enumerate(strategies_with_weights):
|
||||
signal = signals[i]
|
||||
weighted_signal = signal * weight
|
||||
strat_name = strat.name
|
||||
logger.info(f" 策略: {strat_name:<25} | 信号: {signal:6.2f} | 权重: {weight:6.2f} | 加权信号: {weighted_signal:6.2f}")
|
||||
logger.info("--------------------")
|
||||
|
||||
weighted_signal_sum = sum(s * w for s, w in zip(signals, weights))
|
||||
|
||||
buy_threshold = SIGNAL_THRESHOLDS.get('buy_threshold', 1.5)
|
||||
sell_threshold = SIGNAL_THRESHOLDS.get('sell_threshold', -1.5)
|
||||
logger.info(f"加权信号: {weighted_signal_sum:.2f} (买入阈值: {buy_threshold}, 卖出阈值: {sell_threshold})")
|
||||
# logger.info(f"信号比较: {weighted_signal_sum} > {buy_threshold} = {weighted_signal_sum > buy_threshold}")
|
||||
# logger.info(f"信号比较: {weighted_signal_sum} < {sell_threshold} = {weighted_signal_sum < sell_threshold}")
|
||||
|
||||
direction = None
|
||||
if weighted_signal_sum > buy_threshold:
|
||||
direction = "buy"
|
||||
elif weighted_signal_sum < sell_threshold:
|
||||
direction = "sell"
|
||||
|
||||
if direction:
|
||||
logger.info(f"准备执行{direction}交易,信号强度: {weighted_signal_sum:.2f}")
|
||||
success = self.risk_controller.process_trading_signal(direction, current_price, weighted_signal_sum)
|
||||
if not success:
|
||||
logger.warning(f"{direction}交易执行失败")
|
||||
else:
|
||||
logger.info(f"{direction}交易执行成功")
|
||||
|
||||
self.risk_controller.monitor_positions(current_price)
|
||||
|
||||
# --- 状态汇总日志 ---
|
||||
logger.info("--- 财务状况更新 ---")
|
||||
open_positions = self.risk_controller.position_manager.positions
|
||||
if not open_positions:
|
||||
logger.info(" 当前无持仓")
|
||||
else:
|
||||
logger.info(f" 当前持仓: {len(open_positions)} 个")
|
||||
for pos in open_positions:
|
||||
pnl_pct = self.risk_controller.position_manager._calculate_pnl_pct(pos, current_price['last'])
|
||||
# 计算持仓时间
|
||||
holding_time = current_price['time'] - pos['entry_time']
|
||||
holding_minutes = holding_time.total_seconds() / 60
|
||||
logger.info(f" - Ticket {pos['ticket']}: {pos['position_type']} {pos['symbol']} @ {pos['entry_price']:.2f} | 持仓时间: {holding_minutes:.1f}分钟 | 浮动盈亏: {pnl_pct:.2%}")
|
||||
|
||||
trade_summary = self.risk_controller.position_manager.get_trade_summary()
|
||||
if trade_summary and trade_summary['total_trades'] > 0:
|
||||
logger.info(" 已平仓交易摘要:")
|
||||
logger.info(f" - 总交易: {trade_summary['total_trades']}, 盈利: {trade_summary['winning_trades']}, 亏损: {trade_summary['losing_trades']}, 胜率: {trade_summary['win_rate']:.2f}%")
|
||||
logger.info(f" - 总净盈亏: ${trade_summary['total_profit_loss']:.2f}")
|
||||
|
||||
logger.info(f" 总权益: ${self.risk_controller.position_manager.total_equity:.2f}")
|
||||
logger.info("----------------------")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
logger.error(f"交易周期执行失败: {e}\n{traceback.format_exc()}")
|
||||
|
||||
def start(self):
|
||||
if not self._initialize(): return
|
||||
|
||||
logger.info("=== 启动实时交易系统 ===")
|
||||
self.running = True
|
||||
|
||||
while self.running:
|
||||
cycle_start = time.time()
|
||||
self._run_cycle()
|
||||
cycle_time = time.time() - cycle_start
|
||||
wait_time = max(0, self.update_interval - cycle_time)
|
||||
if wait_time > 0: time.sleep(wait_time)
|
||||
|
||||
def stop(self):
|
||||
logger.info("=== 停止实时交易系统 ===")
|
||||
self.running = False
|
||||
try:
|
||||
if self.risk_controller:
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
self.risk_controller.save_trade_history(f"realtime_trades_{timestamp}")
|
||||
except Exception as e:
|
||||
logger.error(f"保存交易记录失败: {e}")
|
||||
finally:
|
||||
self.data_provider.shutdown()
|
||||
logger.info("实时交易系统已停止")
|
||||
sys.exit(0)
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
# 全局变量,用于存储logger实例
|
||||
_logger_instance = None
|
||||
|
||||
def setup_logger():
|
||||
def setup_logger(log_level="INFO"):
|
||||
global _logger_instance
|
||||
if _logger_instance:
|
||||
return _logger_instance
|
||||
@@ -14,24 +14,32 @@ def setup_logger():
|
||||
log_file = os.path.join(log_dir, "strategy.log")
|
||||
|
||||
logger = logging.getLogger("StrategyLogger")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 设置日志级别
|
||||
level_map = {
|
||||
"DEBUG": logging.DEBUG,
|
||||
"INFO": logging.INFO,
|
||||
"WARNING": logging.WARNING,
|
||||
"ERROR": logging.ERROR
|
||||
}
|
||||
logger.setLevel(level_map.get(log_level, logging.INFO))
|
||||
|
||||
# 防止重复添加handler
|
||||
if not logger.handlers:
|
||||
# 输出到控制台
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
|
||||
# 输出到文件
|
||||
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
file_handler.setLevel(level_map.get(log_level, logging.INFO))
|
||||
|
||||
# 输出到控制台
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(level_map.get(log_level, logging.INFO))
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s - [%(levelname)s] - %(message)s')
|
||||
console_handler.setFormatter(formatter)
|
||||
file_handler.setFormatter(formatter)
|
||||
console_handler.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(console_handler)
|
||||
logger.addHandler(file_handler)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
_logger_instance = logger
|
||||
return logger
|
||||
|
||||
@@ -1,248 +1,22 @@
|
||||
import pandas as pd
|
||||
from utils import initialize, shutdown, get_rates, close_all, send_order
|
||||
from backtest import BacktestEngine
|
||||
from core.utils import initialize, shutdown, get_rates
|
||||
from logger import logger
|
||||
from config import INITIAL_CAPITAL, SYMBOL, TIMEFRAME, BACKTEST_COUNT, BACKTEST_START_DATE, BACKTEST_END_DATE, USE_DATE_RANGE
|
||||
from risk_management import RiskController
|
||||
from dynamic_weights import DynamicWeightManager
|
||||
from trade_logger import trade_logger
|
||||
from logger import logger
|
||||
# 导入优化器
|
||||
from optimizer import run_optimizer
|
||||
|
||||
def run_realtime():
|
||||
if not initialize():
|
||||
logger.error("MT5初始化失败")
|
||||
return
|
||||
|
||||
# 初始化风险管理和动态权重
|
||||
risk_controller = RiskController()
|
||||
weight_manager = DynamicWeightManager(risk_controller)
|
||||
|
||||
# 获取动态策略配置
|
||||
strategies_with_weights = weight_manager.get_current_strategies_and_weights()
|
||||
weight_info = weight_manager.get_weight_info()
|
||||
|
||||
logger.info(f"市场状态: {weight_info['market_state']}, 置信度: {weight_info['confidence']:.2f}")
|
||||
|
||||
# 执行策略信号生成
|
||||
signals = []
|
||||
weights = []
|
||||
for strat, weight in strategies_with_weights:
|
||||
try:
|
||||
logger.info(f"执行策略:{strat.__class__.__module__}, 权重: {weight:.2f}")
|
||||
|
||||
# 特殊处理风险管理策略
|
||||
if strat.__class__.__module__ == 'risk_management':
|
||||
signal = strat.generate_signal_with_sync(risk_controller)
|
||||
else:
|
||||
signal = strat.generate_signal()
|
||||
|
||||
signals.append(signal)
|
||||
weights.append(weight)
|
||||
except Exception as e:
|
||||
logger.exception(f"运行策略 {strat.__class__.__module__} 时出错:{e}")
|
||||
|
||||
# 计算加权信号
|
||||
weighted_signal_sum = sum(s * w for s, w in zip(signals, weights))
|
||||
logger.info(f"加权信号总和: {weighted_signal_sum:.2f}")
|
||||
|
||||
# 获取当前价格用于风险管理
|
||||
current_price = get_current_price(SYMBOL)
|
||||
current_time = pd.Timestamp.now()
|
||||
|
||||
# 检查风险管理条件
|
||||
if current_price:
|
||||
risk_action, risk_reason = risk_controller.check_risk_management(current_price)
|
||||
if risk_action != "none":
|
||||
logger.info(f"触发风险管理: {risk_action}, 原因: {risk_reason}")
|
||||
# 记录平仓交易
|
||||
if trade_logger.current_position:
|
||||
trade_logger.close_position(SYMBOL, current_price, current_time, f"risk_management_{risk_action}")
|
||||
risk_controller.execute_risk_action(risk_action, risk_reason)
|
||||
shutdown()
|
||||
return
|
||||
|
||||
# 获取当前持仓状态,取消交易间隔限制
|
||||
current_position = trade_logger.current_position
|
||||
min_trade_interval_minutes = 0 # 取消最小交易间隔限制
|
||||
|
||||
# 取消交易间隔限制检查
|
||||
can_trade = True
|
||||
|
||||
# 调试信息:打印当前状态
|
||||
logger.info(f"当前持仓状态: {current_position is not None}")
|
||||
if current_position:
|
||||
time_since_open = (current_time - current_position['open_time']).total_seconds() / 60
|
||||
logger.info(f"持仓信息: {current_position['direction']} @ {current_position['open_price']:.2f}, 持仓时间: {time_since_open:.1f}分钟")
|
||||
|
||||
# 执行交易决策
|
||||
logger.info(f"加权信号总和: {weighted_signal_sum:.2f}, 买入阈值: 0, 卖出阈值: 0")
|
||||
logger.info(f"风险管理允许买入: {risk_controller.should_allow_trade('buy')}, 允许卖出: {risk_controller.should_allow_trade('sell')}")
|
||||
logger.info(f"允许交易: {can_trade}")
|
||||
|
||||
if weighted_signal_sum > 0:
|
||||
logger.info(f"检测到买入信号: {weighted_signal_sum:.2f} > 0")
|
||||
if risk_controller.should_allow_trade("buy") and can_trade:
|
||||
# 检查是否已经有相同方向的持仓
|
||||
if current_position and current_position['direction'] == 'buy':
|
||||
logger.info(f"已持有多头仓位,信号强度: {weighted_signal_sum:.2f},不重复开仓")
|
||||
else:
|
||||
logger.info(f"=== 准备执行买入交易 ===")
|
||||
logger.info(f"买入信号: 加权总和({weighted_signal_sum:.2f}) > 0")
|
||||
logger.info(f"当前价格: {current_price:.2f}")
|
||||
logger.info(f"风险管理允许: {risk_controller.should_allow_trade('buy')}")
|
||||
logger.info(f"交易间隔检查通过: {can_trade}")
|
||||
|
||||
# 先平掉现有仓位(如果有)
|
||||
if current_position:
|
||||
close_all(SYMBOL)
|
||||
trade_logger.close_position(SYMBOL, current_price, current_time, "close_before_buy")
|
||||
|
||||
# 开新仓
|
||||
send_order(SYMBOL, 'buy')
|
||||
trade_logger.open_position(SYMBOL, 'buy', current_price, current_time, f"weighted_signal_{weighted_signal_sum:.2f}")
|
||||
|
||||
if current_price:
|
||||
risk_controller.update_position_entry(current_price, "long")
|
||||
else:
|
||||
if not can_trade:
|
||||
logger.info(f"买入信号被阻止: 交易间隔限制")
|
||||
else:
|
||||
logger.info(f"买入信号被风险管理阻止: risk_controller.should_allow_trade('buy') = {risk_controller.should_allow_trade('buy')}")
|
||||
|
||||
elif weighted_signal_sum < 0:
|
||||
logger.info(f"检测到卖出信号: {weighted_signal_sum:.2f} < 0")
|
||||
if risk_controller.should_allow_trade("sell") and can_trade:
|
||||
# 检查是否已经有相同方向的持仓
|
||||
if current_position and current_position['direction'] == 'sell':
|
||||
logger.info(f"已持有空头仓位,信号强度: {weighted_signal_sum:.2f},不重复开仓")
|
||||
else:
|
||||
logger.info(f"=== 准备执行卖出交易 ===")
|
||||
logger.info(f"卖出信号: 加权总和({weighted_signal_sum:.2f}) < 0")
|
||||
logger.info(f"当前价格: {current_price:.2f}")
|
||||
logger.info(f"风险管理允许: {risk_controller.should_allow_trade('sell')}")
|
||||
logger.info(f"交易间隔检查通过: {can_trade}")
|
||||
|
||||
# 先平掉现有仓位(如果有)
|
||||
if current_position:
|
||||
close_all(SYMBOL)
|
||||
trade_logger.close_position(SYMBOL, current_price, current_time, "close_before_sell")
|
||||
|
||||
# 开新仓
|
||||
send_order(SYMBOL, 'sell')
|
||||
trade_logger.open_position(SYMBOL, 'sell', current_price, current_time, f"weighted_signal_{weighted_signal_sum:.2f}")
|
||||
|
||||
if current_price:
|
||||
risk_controller.update_position_entry(current_price, "short")
|
||||
else:
|
||||
logger.info(f"卖出信号被阻止")
|
||||
else:
|
||||
# 信号在中间区域,使用RiskController决定是否平仓
|
||||
if current_position:
|
||||
# 使用RiskController检查风险管理条件
|
||||
risk_action, risk_reason = risk_controller.check_risk_management(current_price)
|
||||
|
||||
if risk_action != "none":
|
||||
close_all(SYMBOL)
|
||||
trade_logger.close_position(SYMBOL, current_price, current_time, f"signal_neutral_{risk_action}")
|
||||
risk_controller.execute_risk_action(risk_action, risk_reason)
|
||||
|
||||
shutdown()
|
||||
|
||||
def get_current_price(symbol):
|
||||
"""获取当前价格"""
|
||||
try:
|
||||
import MetaTrader5 as mt5
|
||||
tick = mt5.symbol_info_tick(symbol)
|
||||
return tick.bid if tick else None
|
||||
except Exception as e:
|
||||
logger.error(f"获取当前价格失败: {e}")
|
||||
return None
|
||||
|
||||
def run_backtest():
|
||||
if not initialize():
|
||||
logger.error("MT5初始化失败")
|
||||
return
|
||||
|
||||
# 重置交易日志记录器
|
||||
trade_logger.__init__()
|
||||
|
||||
# 根据配置选择获取数据的方式
|
||||
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)
|
||||
|
||||
if rates is None:
|
||||
logger.error("获取历史数据失败")
|
||||
shutdown()
|
||||
return
|
||||
|
||||
logger.info(f"初始资金: {INITIAL_CAPITAL}")
|
||||
|
||||
df = pd.DataFrame(rates)
|
||||
engine = BacktestEngine(df)
|
||||
|
||||
# 使用动态权重管理器(需要在MT5连接状态下获取市场状态)
|
||||
risk_controller = RiskController()
|
||||
weight_manager = DynamicWeightManager(risk_controller)
|
||||
|
||||
# 获取当前策略配置(回测时使用固定权重或模拟动态权重)
|
||||
strategies_with_weights = weight_manager.get_current_strategies_and_weights()
|
||||
weight_info = weight_manager.get_weight_info()
|
||||
|
||||
logger.info(f"回测使用市场状态: {weight_info['market_state']}")
|
||||
|
||||
# 获取市场状态后关闭MT5连接
|
||||
shutdown()
|
||||
|
||||
signals_list = []
|
||||
weights = []
|
||||
strategy_names = []
|
||||
|
||||
for strat, weight in strategies_with_weights:
|
||||
try:
|
||||
logger.info(f"回测策略:{strat.__class__.__module__}, 权重: {weight:.2f}")
|
||||
signals = engine.run_strategy(strat)
|
||||
signals_list.append(signals)
|
||||
weights.append(weight)
|
||||
strategy_names.append(strat.__class__.__module__)
|
||||
except Exception as e:
|
||||
logger.exception(f"回测策略 {strat.__class__.__module__} 时出错:{e}")
|
||||
|
||||
combined_signal = engine.combine_signals(signals_list, weights)
|
||||
|
||||
# 使用带交易记录的收益率计算
|
||||
cum_ret = engine.calc_returns_with_trades(combined_signal, SYMBOL)
|
||||
final_capital = INITIAL_CAPITAL * (1 + cum_ret.iloc[-1])
|
||||
|
||||
logger.info("策略组合回测完成")
|
||||
logger.info(f"最终资金: {final_capital:.2f}")
|
||||
logger.info(f"总收益率: {(final_capital/INITIAL_CAPITAL - 1):.2%}")
|
||||
logger.info("使用的策略权重:")
|
||||
for name, weight in zip(strategy_names, weights):
|
||||
logger.info(f" {name}: {weight:.2f}")
|
||||
logger.info("最近收益率:")
|
||||
logger.info(cum_ret.tail())
|
||||
|
||||
# 打印详细的交易记录
|
||||
logger.info("\n" + "="*50)
|
||||
logger.info("详细交易记录:")
|
||||
trade_logger.print_all_trades()
|
||||
|
||||
# 保存交易记录到文件
|
||||
trade_logger.save_to_csv("backtest_trades.csv")
|
||||
trade_logger.save_to_json("backtest_trades.json")
|
||||
|
||||
# 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. 运行一次回测 (使用config.py中的默认权重)
|
||||
run_backtest()
|
||||
# 1. 运行回测, 请执行 python start_backtest.py
|
||||
# run_backtest()
|
||||
|
||||
# 2. 运行实盘交易 (使用config.py中的默认权重)
|
||||
# run_realtime()
|
||||
# 2. 运行实盘交易, 请执行 python start_realtime.py
|
||||
# from realtime_trader import RealtimeTrader
|
||||
# trader = RealtimeTrader()
|
||||
# trader.start()
|
||||
|
||||
# 3. 运行遗传算法优化,寻找最佳权重
|
||||
# run_optimizer()
|
||||
pass
|
||||
+696
-110
@@ -1,135 +1,721 @@
|
||||
|
||||
import random
|
||||
import numpy as np
|
||||
from deap import base, creator, tools, algorithms
|
||||
import multiprocessing
|
||||
|
||||
import pandas as pd
|
||||
from utils import initialize, shutdown, get_rates
|
||||
from backtest import BacktestEngine
|
||||
from logger import logger
|
||||
from config import INITIAL_CAPITAL, SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE, USE_DATE_RANGE
|
||||
from risk_management import RiskController
|
||||
from dynamic_weights import DynamicWeightManager
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
from tqdm import tqdm
|
||||
|
||||
# 1. 定义适应度函数 (已优化)
|
||||
def evaluate_fitness(individual, df_data):
|
||||
"""
|
||||
评估函数现在接收预先加载的DataFrame作为参数,避免了重复IO。
|
||||
使用新的动态权重架构进行评估。
|
||||
输入:
|
||||
- individual: 一个代表策略权重的列表。
|
||||
- df_data: 包含历史K线数据的Pandas DataFrame。
|
||||
输出: 一个元组,包含适应度分数(最终资金)。
|
||||
"""
|
||||
weights = individual
|
||||
# 固定随机种子,确保可复现
|
||||
SEED = 42
|
||||
random.seed(SEED)
|
||||
np.random.seed(SEED)
|
||||
|
||||
# 使用新的架构进行评估
|
||||
engine = BacktestEngine(df_data)
|
||||
|
||||
# 创建风险管理器和权重管理器
|
||||
risk_controller = RiskController()
|
||||
weight_manager = DynamicWeightManager(risk_controller)
|
||||
|
||||
# 获取策略实例
|
||||
strategies_with_weights = weight_manager.get_current_strategies_and_weights()
|
||||
|
||||
# 使用优化器提供的权重替换动态权重
|
||||
signals_list = []
|
||||
strategy_names = []
|
||||
|
||||
for i, (strat, _) in enumerate(strategies_with_weights):
|
||||
if i < len(weights):
|
||||
signals = engine.run_strategy(strat)
|
||||
signals_list.append(signals)
|
||||
strategy_names.append(strat.__class__.__module__)
|
||||
|
||||
# 使用优化器权重进行组合
|
||||
combined_signal = engine.combine_signals(signals_list, weights[:len(signals_list)])
|
||||
cum_ret = engine.calc_returns(combined_signal)
|
||||
final_capital = INITIAL_CAPITAL * (1 + cum_ret.iloc[-1])
|
||||
# Add project root to path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# 在优化过程中,可以注释掉这行日志以提高速度,因为它会大量输出
|
||||
# logger.info(f"评估权重: {[f'{w:.2f}' for w in weights[:len(signals_list)]]} -> 最终资金: {final_capital:.2f}")
|
||||
# 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
|
||||
|
||||
return (final_capital,)
|
||||
# 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
|
||||
|
||||
# 2. 设置遗传算法 (已优化)
|
||||
def run_optimizer():
|
||||
"""
|
||||
配置并运行遗传算法
|
||||
"""
|
||||
# --- 数据预加载 ---
|
||||
logger.info("--- 开始遗传算法优化 --- ")
|
||||
logger.info("步骤 1/4: 初始化MT5并预加载历史数据...")
|
||||
if not initialize():
|
||||
logger.error("MT5初始化失败,无法开始优化")
|
||||
return
|
||||
|
||||
# 根据配置选择获取数据的方式
|
||||
if USE_DATE_RANGE:
|
||||
rates = get_rates(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT, OPTIMIZER_START_DATE, OPTIMIZER_END_DATE)
|
||||
else:
|
||||
rates = get_rates(SYMBOL, TIMEFRAME, OPTIMIZER_COUNT)
|
||||
shutdown() # 获取数据后即可关闭连接
|
||||
# --- 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:
|
||||
logger.error("获取历史数据失败,优化终止")
|
||||
return
|
||||
print("获取历史数据失败,退出。")
|
||||
sys.exit(1)
|
||||
|
||||
df_historical_data = pd.DataFrame(rates)
|
||||
logger.info(f"历史数据加载完成,共 {len(df_historical_data)} 条记录。")
|
||||
|
||||
# --- DEAP 设置 ---
|
||||
logger.info("步骤 2/4: 配置遗传算法...")
|
||||
creator.create("FitnessMax", base.Fitness, weights=(1.0,))
|
||||
creator.create("Individual", list, fitness=creator.FitnessMax)
|
||||
|
||||
toolbox = base.Toolbox()
|
||||
toolbox.register("attr_float", random.uniform, 0.1, 2.0)
|
||||
df = pd.DataFrame(rates)
|
||||
df.set_index(pd.to_datetime(df['time'], unit='s'), inplace=True)
|
||||
|
||||
# 使用动态权重管理器获取策略数量
|
||||
risk_controller = RiskController()
|
||||
weight_manager = DynamicWeightManager(risk_controller)
|
||||
num_strategies = len(weight_manager.list_available_strategies())
|
||||
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'},
|
||||
|
||||
toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_float, n=num_strategies)
|
||||
toolbox.register("population", tools.initRepeat, list, toolbox.individual)
|
||||
# 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'},
|
||||
|
||||
toolbox.register("evaluate", evaluate_fitness, df_data=df_historical_data)
|
||||
toolbox.register("mate", tools.cxTwoPoint)
|
||||
toolbox.register("mutate", tools.mutGaussian, mu=0, sigma=0.5, indpb=0.2)
|
||||
toolbox.register("select", tools.selTournament, tournsize=3)
|
||||
# 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'},
|
||||
|
||||
# --- 并行计算设置 ---
|
||||
logger.info("步骤 3/4: 配置并行计算和统计...")
|
||||
pool = multiprocessing.Pool()
|
||||
toolbox.register("map", pool.map)
|
||||
# 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'},
|
||||
|
||||
# --- 统计功能设置 ---
|
||||
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)
|
||||
# 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'},
|
||||
|
||||
# --- 运行算法 ---
|
||||
population = toolbox.population(n=50)
|
||||
ngen = 20
|
||||
cxpb = 0.5
|
||||
mutpb = 0.2
|
||||
# MomentumBreakoutStrategy parameters
|
||||
{'name': 'momentum_breakout_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'MomentumBreakoutStrategy'},
|
||||
|
||||
logger.info(f"步骤 4/4: 开始并行进化... (种群大小: {len(population)}, 进化代数: {ngen})")
|
||||
algorithms.eaSimple(population, toolbox, cxpb, mutpb, ngen, stats=stats, verbose=True)
|
||||
# KDJStrategy parameters
|
||||
{'name': 'kdj_period', 'type': 'int', 'min': 5, 'max': 21, 'strategy': 'KDJStrategy'},
|
||||
|
||||
pool.close()
|
||||
# TurtleStrategy parameters
|
||||
{'name': 'turtle_period', 'type': 'int', 'min': 10, 'max': 50, 'strategy': 'TurtleStrategy'},
|
||||
|
||||
# --- 结果 ---
|
||||
best_individual = tools.selBest(population, k=1)[0]
|
||||
best_fitness = best_individual.fitness.values[0]
|
||||
# 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'},
|
||||
|
||||
logger.info("--- 遗传算法优化结束 ---")
|
||||
logger.info(f"找到的最佳权重: {[f'{w:.2f}' for w in best_individual]}")
|
||||
logger.info(f"对应的最佳最终资金: {best_fitness:.2f}")
|
||||
# DailyBreakoutStrategy parameters
|
||||
{'name': 'daily_breakout_bars_count', 'type': 'int', 'min': 720, 'max': 2880, 'strategy': 'DailyBreakoutStrategy'},
|
||||
|
||||
return best_individual, best_fitness
|
||||
# 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()
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
ma_cross_short_window: 5
|
||||
ma_cross_long_window: 39
|
||||
rsi_period: 22
|
||||
rsi_overbought: 80
|
||||
rsi_oversold: 24
|
||||
bollinger_period: 10
|
||||
bollinger_std_dev: 2.8916513144581044
|
||||
macd_fast_ema: 17
|
||||
macd_slow_ema: 22
|
||||
macd_signal_period: 13
|
||||
mean_reversion_period: 40
|
||||
mean_reversion_std_dev: 2.917216289407233
|
||||
momentum_breakout_period: 27
|
||||
kdj_period: 6
|
||||
turtle_period: 16
|
||||
wave_ema_short: 3
|
||||
wave_ema_medium: 15
|
||||
wave_ema_long: 36
|
||||
wave_period: 28
|
||||
wave_range_period: 30
|
||||
wave_adx_period: 21
|
||||
wave_momentum_period: 13
|
||||
wave_range_threshold: -0.02028040971155598
|
||||
wave_adx_threshold: 22
|
||||
daily_breakout_bars_count: 807
|
||||
buy_threshold: 2.52197653160939
|
||||
sell_threshold: -2.8041541426704852
|
||||
stop_loss_pct: -0.16609967085382554
|
||||
profit_retracement_pct: 0.1
|
||||
min_profit_for_trailing: 0.05431287857723996
|
||||
take_profit_pct: 0.049359092938974364
|
||||
max_holding_minutes: 79
|
||||
min_profit_for_time_exit: -0.051349429573099285
|
||||
weight_MACrossStrategy: 0.44428771408324463
|
||||
weight_RSIStrategy: 1.6518277343219148
|
||||
weight_BollingerStrategy: 0.6014474871460991
|
||||
weight_MeanReversionStrategy: 0.04509729271515517
|
||||
weight_MomentumBreakoutStrategy: 1.0768245883204248
|
||||
weight_MACDStrategy: 0.8909161638775971
|
||||
weight_KDJStrategy: 1.095784002572773
|
||||
weight_TurtleStrategy: 1.7942934575029192
|
||||
weight_DailyBreakoutStrategy: 2.4237777692491713
|
||||
weight_WaveTheoryStrategy: 0.7354173414675755
|
||||
market_trend_period: 44
|
||||
market_retracement_tolerance: 0.1382775008306345
|
||||
market_volume_period: 13
|
||||
market_volume_ma_period: 12
|
||||
trend_price_breakout_weight: 0.03552729699860058
|
||||
trend_volume_confirmation_weight: 0.2564191814903339
|
||||
trend_momentum_oscillator_weight: 0.48690789892001296
|
||||
trend_moving_average_weight: 0.3549274308680269
|
||||
trend_strong_threshold: 0.24063407379490956
|
||||
trend_weak_threshold: 0.3708545035763192
|
||||
trend_volume_spike: 1.7781348694952452
|
||||
trend_oversold: 29
|
||||
trend_overbought: 69
|
||||
confidence_high: 0.941049792261965
|
||||
confidence_medium: 0.8881796011658835
|
||||
@@ -1,3 +1,4 @@
|
||||
MetaTrader5
|
||||
pandas
|
||||
deap
|
||||
tqdm
|
||||
@@ -1,93 +0,0 @@
|
||||
from .market_state import MarketStateAnalyzer
|
||||
from .position_manager import PositionManager
|
||||
from logger import logger
|
||||
from config import RISK_CONFIG
|
||||
|
||||
class RiskController:
|
||||
"""
|
||||
风险管理控制器,统一管理市场状态分析和仓位管理
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.market_state_analyzer = MarketStateAnalyzer()
|
||||
self.position_manager = PositionManager()
|
||||
|
||||
# 从配置文件读取风险控制参数
|
||||
self.max_position_size = RISK_CONFIG.get("max_position_size", 0.1)
|
||||
self.max_daily_loss = RISK_CONFIG.get("max_daily_loss", -0.05)
|
||||
self.daily_pnl = 0.0 # 日内盈亏
|
||||
|
||||
def get_market_state(self):
|
||||
"""
|
||||
获取当前市场状态
|
||||
"""
|
||||
return self.market_state_analyzer.get_market_state()
|
||||
|
||||
def get_dynamic_weights(self):
|
||||
"""
|
||||
获取动态策略权重
|
||||
"""
|
||||
market_state, confidence = self.get_market_state()
|
||||
logger.info(f"当前市场状态: {market_state}, 置信度: {confidence:.2f}")
|
||||
|
||||
weights = self.market_state_analyzer.get_strategy_weights(market_state, confidence)
|
||||
logger.info(f"动态权重配置: {weights}")
|
||||
|
||||
return weights, market_state, confidence
|
||||
|
||||
def check_risk_management(self, current_price):
|
||||
"""
|
||||
检查风险管理条件
|
||||
"""
|
||||
return self.position_manager.check_risk_management(current_price)
|
||||
|
||||
def execute_risk_action(self, action, reason):
|
||||
"""
|
||||
执行风险管理操作
|
||||
"""
|
||||
self.position_manager.execute_risk_action(action, reason)
|
||||
|
||||
def update_position_entry(self, entry_price, position_type="long"):
|
||||
"""
|
||||
更新持仓入场信息
|
||||
"""
|
||||
self.position_manager.update_position_entry(entry_price, position_type)
|
||||
|
||||
def should_allow_trade(self, signal_type):
|
||||
"""
|
||||
根据风险控制决定是否允许交易
|
||||
"""
|
||||
# 检查日亏损限制
|
||||
if self.daily_pnl <= self.max_daily_loss:
|
||||
logger.warning(f"日亏损已达{self.daily_pnl:.2%},暂停交易")
|
||||
return False
|
||||
|
||||
# 检查持仓数量限制
|
||||
current_positions = len(self.position_manager.positions)
|
||||
if current_positions >= 1: # 限制同时只持有一个仓位
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_daily_pnl(self, pnl_change):
|
||||
"""
|
||||
更新日内盈亏
|
||||
"""
|
||||
self.daily_pnl += pnl_change
|
||||
logger.info(f"更新日内盈亏: {self.daily_pnl:.2%}")
|
||||
|
||||
def reset_daily_stats(self):
|
||||
"""
|
||||
重置日内统计
|
||||
"""
|
||||
self.daily_pnl = 0.0
|
||||
logger.info("重置日内统计")
|
||||
|
||||
def reset_all_states(self):
|
||||
"""
|
||||
重置所有状态
|
||||
"""
|
||||
self.market_state_analyzer.reset_state()
|
||||
self.position_manager.reset_positions()
|
||||
self.reset_daily_stats()
|
||||
logger.info("重置所有风险管理状态")
|
||||
@@ -1,312 +0,0 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import MetaTrader5 as mt5
|
||||
from logger import logger
|
||||
from utils import get_rates
|
||||
from config import MARKET_STATE_CONFIG, SYMBOL, DEFAULT_WEIGHTS, TREND_INDICATOR_WEIGHTS, TREND_THRESHOLDS
|
||||
|
||||
class MarketStateAnalyzer:
|
||||
"""
|
||||
市场状态分析器,基于resilient_trend逻辑
|
||||
判断当前市场状态:uptrend, downtrend, none
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.symbol = SYMBOL
|
||||
self.timeframe = mt5.TIMEFRAME_H1 # 使用小时线数据
|
||||
self.hourly_data_count = 100 # 获取最近100小时的数据
|
||||
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.current_trend = "none" # none, uptrend, downtrend
|
||||
self.trend_peak = 0.0
|
||||
self.trend_trough = float('inf')
|
||||
|
||||
# 趋势指标权重
|
||||
self.indicator_weights = TREND_INDICATOR_WEIGHTS
|
||||
self.thresholds = TREND_THRESHOLDS
|
||||
|
||||
def calculate_volume_indicators(self, df):
|
||||
"""计算成交量指标"""
|
||||
# MT5使用tick_volume和real_volume,这里使用tick_volume作为成交量
|
||||
df['volume'] = df['tick_volume'] # 将tick_volume映射为volume以便后续计算
|
||||
|
||||
# 成交量移动平均
|
||||
df['volume_ma'] = df['volume'].rolling(self.volume_ma_period).mean()
|
||||
|
||||
# 成交量相对强度
|
||||
df['volume_ratio'] = df['volume'] / df['volume_ma']
|
||||
|
||||
# 价量相关性 (最近n期)
|
||||
volume_corr = df['close'].rolling(self.volume_period).corr(df['volume'])
|
||||
|
||||
return df, volume_corr.iloc[-1] if len(volume_corr) > 0 else 0
|
||||
|
||||
def calculate_momentum_indicators(self, df):
|
||||
"""计算动量指标"""
|
||||
# RSI
|
||||
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
|
||||
df['rsi'] = 100 - (100 / (1 + rs))
|
||||
|
||||
# MACD
|
||||
exp1 = df['close'].ewm(span=12).mean()
|
||||
exp2 = df['close'].ewm(span=26).mean()
|
||||
df['macd'] = exp1 - exp2
|
||||
df['macd_signal'] = df['macd'].ewm(span=9).mean()
|
||||
df['macd_histogram'] = df['macd'] - df['macd_signal']
|
||||
|
||||
# 移动平均线
|
||||
df['ma20'] = df['close'].rolling(20).mean()
|
||||
df['ma50'] = df['close'].rolling(50).mean()
|
||||
|
||||
return df
|
||||
|
||||
def calculate_price_breakout_score(self, df):
|
||||
"""计算价格突破得分 (0-1)"""
|
||||
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]
|
||||
|
||||
# 计算价格在区间中的位置
|
||||
price_position = (current_price - low_period) / (high_period - low_period)
|
||||
|
||||
# 突破确认
|
||||
if current_price > high_period:
|
||||
return 0.8 # 向上突破
|
||||
elif current_price < low_period:
|
||||
return 0.2 # 向下突破
|
||||
else:
|
||||
return price_position # 在区间内的位置
|
||||
|
||||
def calculate_volume_score(self, df, volume_corr):
|
||||
"""计算成交量确认得分 (0-1)"""
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
def calculate_momentum_score(self, df):
|
||||
"""计算动量指标得分 (0-1)"""
|
||||
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
|
||||
|
||||
score = 0.5 # 基础分数
|
||||
|
||||
# RSI判断
|
||||
if current_rsi > self.thresholds['overbought']:
|
||||
score += 0.2 # 超买,可能继续上涨
|
||||
elif current_rsi < self.thresholds['oversold']:
|
||||
score -= 0.2 # 超卖,可能继续下跌
|
||||
|
||||
# MACD直方图判断
|
||||
if current_macd_hist > 0:
|
||||
score += 0.2 # MACD正向动能
|
||||
else:
|
||||
score -= 0.2 # MACD负向动能
|
||||
|
||||
return np.clip(score, 0, 1)
|
||||
|
||||
def calculate_ma_score(self, df):
|
||||
"""计算移动平均线得分 (0-1)"""
|
||||
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 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)
|
||||
|
||||
def get_market_state(self):
|
||||
"""
|
||||
获取当前市场状态 - 使用加权多指标分析
|
||||
返回: (state, confidence)
|
||||
state: "uptrend", "downtrend", "none", "ranging"
|
||||
confidence: 0.0-1.0 的置信度
|
||||
"""
|
||||
logger.info(f"开始获取市场状态数据: {self.symbol}, 时间周期: {self.timeframe}, 数据量: {self.hourly_data_count}")
|
||||
|
||||
rates = get_rates(self.symbol, self.timeframe, self.hourly_data_count)
|
||||
|
||||
# 检查数据获取状态
|
||||
if rates is None:
|
||||
logger.error(f"获取市场数据失败: {self.symbol}")
|
||||
return "none", 0.0
|
||||
|
||||
logger.info(f"成功获取数据,数据量: {len(rates)}, 需要最小数据量: {max(self.trend_period, 50)}")
|
||||
|
||||
if len(rates) < max(self.trend_period, 50):
|
||||
logger.warning(f"数据量不足: 当前{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)
|
||||
|
||||
logger.info(f"各项指标得分: 价格突破={price_score:.3f}, 成交量确认={volume_score:.3f}, "
|
||||
f"动量指标={momentum_score:.3f}, 移动平均={ma_score:.3f}")
|
||||
|
||||
# 加权计算趋势强度
|
||||
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']
|
||||
)
|
||||
|
||||
logger.info(f"趋势强度计算结果: {trend_strength:.3f}")
|
||||
logger.info(f"阈值参考: 强趋势={self.thresholds['strong_trend']}, 弱趋势={self.thresholds['weak_trend']}")
|
||||
|
||||
# 判断市场状态
|
||||
if trend_strength >= self.thresholds['strong_trend']:
|
||||
state = "uptrend"
|
||||
confidence = min(0.9, trend_strength)
|
||||
logger.info(f"判断为上涨趋势: 趋势强度{trend_strength:.3f} >= 强趋势阈值{self.thresholds['strong_trend']}")
|
||||
elif trend_strength <= (1 - self.thresholds['strong_trend']):
|
||||
state = "downtrend"
|
||||
confidence = min(0.9, 1 - trend_strength)
|
||||
logger.info(f"判断为下跌趋势: 趋势强度{trend_strength:.3f} <= {1 - self.thresholds['strong_trend']:.3f}")
|
||||
elif trend_strength >= self.thresholds['weak_trend']:
|
||||
state = "ranging"
|
||||
confidence = 0.6
|
||||
logger.info(f"判断为震荡市场: 趋势强度{trend_strength:.3f} 在弱趋势阈值{self.thresholds['weak_trend']}和强趋势阈值{self.thresholds['strong_trend']}之间")
|
||||
else:
|
||||
state = "none"
|
||||
confidence = 0.4
|
||||
logger.info(f"判断为无明确趋势: 趋势强度{trend_strength:.3f} 低于弱趋势阈值{self.thresholds['weak_trend']}")
|
||||
|
||||
# 更新趋势状态(用于状态机逻辑)
|
||||
self.update_trend_state(df, trend_strength, state)
|
||||
|
||||
logger.debug(f"市场状态分析: {state}, 置信度: {confidence:.2f}, "
|
||||
f"趋势强度: {trend_strength:.2f}, "
|
||||
f"价格得分: {price_score:.2f}, 成交量得分: {volume_score:.2f}, "
|
||||
f"动量得分: {momentum_score:.2f}, 均线得分: {ma_score:.2f}")
|
||||
|
||||
return state, confidence
|
||||
|
||||
def update_trend_state(self, df, trend_strength, new_state):
|
||||
"""更新趋势状态(保持原有状态机逻辑的兼容性)"""
|
||||
current_price = df['close'].iloc[-1]
|
||||
|
||||
if new_state == "uptrend":
|
||||
self.current_trend = "uptrend"
|
||||
self.trend_peak = max(self.trend_peak, current_price)
|
||||
elif new_state == "downtrend":
|
||||
self.current_trend = "downtrend"
|
||||
self.trend_trough = min(self.trend_trough, current_price)
|
||||
else:
|
||||
# 检查是否需要反转趋势
|
||||
if self.current_trend == "uptrend":
|
||||
if current_price < self.trend_peak * (1 - self.retracement_tolerance):
|
||||
self.current_trend = "none"
|
||||
elif self.current_trend == "downtrend":
|
||||
if current_price > self.trend_trough * (1 + self.retracement_tolerance):
|
||||
self.current_trend = "none"
|
||||
|
||||
def get_strategy_weights(self, market_state, confidence):
|
||||
"""
|
||||
根据市场状态返回推荐的策略权重配置
|
||||
"""
|
||||
weight_configs = {
|
||||
"uptrend": {
|
||||
"ma_cross": 2.5,
|
||||
"momentum_breakout": 2.2,
|
||||
"turtle": 2.0,
|
||||
"macd": 1.8,
|
||||
"rsi": 1.2,
|
||||
"bollinger": 1.0,
|
||||
"kdj": 0.8,
|
||||
"mean_reversion": 0.5,
|
||||
"daily_breakout": 1.5,
|
||||
"wave_theory": 1.8 # 趋势市中波浪理论权重适中
|
||||
},
|
||||
"downtrend": {
|
||||
"ma_cross": 2.5,
|
||||
"turtle": 2.2,
|
||||
"momentum_breakout": 2.0,
|
||||
"macd": 1.8,
|
||||
"rsi": 1.5,
|
||||
"bollinger": 1.2,
|
||||
"kdj": 1.0,
|
||||
"mean_reversion": 0.8,
|
||||
"daily_breakout": 1.0,
|
||||
"wave_theory": 1.8 # 趋势市中波浪理论权重适中
|
||||
},
|
||||
"ranging": {
|
||||
"rsi": 2.5,
|
||||
"bollinger": 2.2,
|
||||
"mean_reversion": 2.0,
|
||||
"kdj": 1.8,
|
||||
"wave_theory": 3.0, # 震荡市中波浪理论权重最高
|
||||
"ma_cross": 1.2,
|
||||
"macd": 1.0,
|
||||
"turtle": 0.8,
|
||||
"momentum_breakout": 0.5,
|
||||
"daily_breakout": 1.5
|
||||
},
|
||||
"none": DEFAULT_WEIGHTS
|
||||
}
|
||||
|
||||
base_weights = weight_configs.get(market_state, weight_configs["none"])
|
||||
|
||||
# 根据置信度调整权重
|
||||
if confidence > 0.7:
|
||||
# 高置信度,使用推荐权重
|
||||
return base_weights
|
||||
elif confidence > 0.4:
|
||||
# 中等置信度,混合默认权重
|
||||
default_weights = weight_configs["none"]
|
||||
return {k: (base_weights[k] * 0.7 + default_weights[k] * 0.3)
|
||||
for k in base_weights}
|
||||
else:
|
||||
# 低置信度,使用默认权重
|
||||
return weight_configs["none"]
|
||||
|
||||
def reset_state(self):
|
||||
"""重置市场状态"""
|
||||
self.current_trend = "none"
|
||||
self.trend_peak = 0.0
|
||||
self.trend_trough = float('inf')
|
||||
@@ -1,105 +0,0 @@
|
||||
import pandas as pd
|
||||
from logger import logger
|
||||
from utils import has_open_position, close_all
|
||||
from config import RISK_CONFIG, SYMBOL
|
||||
|
||||
class PositionManager:
|
||||
"""
|
||||
仓位管理器,基于profit_protect逻辑
|
||||
处理止损、止盈、追踪止损等风险管理
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.symbol = SYMBOL
|
||||
|
||||
# 从配置文件读取风险管理参数
|
||||
self.stop_loss_pct = RISK_CONFIG.get("stop_loss_pct", -0.10)
|
||||
self.profit_retracement_pct = RISK_CONFIG.get("profit_retracement_pct", 0.30)
|
||||
self.min_profit_for_trailing = RISK_CONFIG.get("min_profit_for_trailing", 0.05)
|
||||
self.take_profit_pct = RISK_CONFIG.get("take_profit_pct", 0.20)
|
||||
|
||||
# 持仓状态
|
||||
self.positions = {} # symbol: {entry_price, entry_time, peak_profit}
|
||||
|
||||
def check_risk_management(self, current_price):
|
||||
"""
|
||||
检查是否需要触发风险管理操作
|
||||
返回: action ("close_long", "close_short", "none"), reason
|
||||
"""
|
||||
# 在回测环境中,使用self.positions来判断持仓状态
|
||||
# 而不是使用has_open_position()(该函数依赖MT5实时数据)
|
||||
position_info = self.positions.get(self.symbol)
|
||||
if not position_info:
|
||||
return "none", "no_position"
|
||||
|
||||
entry_price = position_info['entry_price']
|
||||
current_profit_pct = (current_price - entry_price) / entry_price
|
||||
|
||||
# 更新最高利润
|
||||
self.positions[self.symbol]['peak_profit'] = max(
|
||||
position_info.get('peak_profit', 0),
|
||||
current_profit_pct
|
||||
)
|
||||
peak_profit = self.positions[self.symbol]['peak_profit']
|
||||
|
||||
# 检查止损条件 - 仅基于买入成本判断亏损,不考虑盈利回撤
|
||||
if current_profit_pct < 0 and current_profit_pct <= self.stop_loss_pct:
|
||||
reason = f"止损触发:当前亏损{current_profit_pct:.2%}"
|
||||
action = "close_long" if current_profit_pct < 0 else "close_short"
|
||||
return action, reason
|
||||
|
||||
# 检查固定止盈条件
|
||||
if current_profit_pct >= self.take_profit_pct:
|
||||
reason = f"止盈触发:当前盈利{current_profit_pct:.2%}"
|
||||
action = "close_long" if current_profit_pct > 0 else "close_short"
|
||||
return action, reason
|
||||
|
||||
# 检查追踪止损条件 - 只有在利润达到min_profit_for_trailing以上才激活
|
||||
if peak_profit > self.min_profit_for_trailing:
|
||||
retracement_from_peak = peak_profit - current_profit_pct
|
||||
if peak_profit > 0:
|
||||
retracement_pct = retracement_from_peak / peak_profit
|
||||
if retracement_pct >= self.profit_retracement_pct:
|
||||
reason = f"追踪止损:最高盈利{peak_profit:.2%},回撤{retracement_pct:.2%}"
|
||||
action = "close_long" if current_profit_pct > 0 else "close_short"
|
||||
return action, reason
|
||||
|
||||
return "none", "no_action_needed"
|
||||
|
||||
def execute_risk_action(self, action, reason):
|
||||
"""
|
||||
执行风险管理操作
|
||||
"""
|
||||
if action == "close_long":
|
||||
logger.info(f"执行平多仓:{reason}")
|
||||
close_all(self.symbol)
|
||||
self.positions.pop(self.symbol, None)
|
||||
elif action == "close_short":
|
||||
logger.info(f"执行平空仓:{reason}")
|
||||
close_all(self.symbol)
|
||||
self.positions.pop(self.symbol, None)
|
||||
|
||||
def update_position_entry(self, entry_price, position_type="long"):
|
||||
"""
|
||||
更新持仓入场信息
|
||||
"""
|
||||
self.positions[self.symbol] = {
|
||||
'entry_price': entry_price,
|
||||
'entry_time': pd.Timestamp.now(),
|
||||
'position_type': position_type,
|
||||
'peak_profit': 0.0
|
||||
}
|
||||
logger.info(f"记录持仓入场:价格={entry_price:.2f}, 类型={position_type}")
|
||||
|
||||
def get_position_info(self):
|
||||
"""
|
||||
获取当前持仓信息
|
||||
"""
|
||||
return self.positions.get(self.symbol, None)
|
||||
|
||||
def reset_positions(self):
|
||||
"""
|
||||
重置所有持仓状态
|
||||
"""
|
||||
self.positions.clear()
|
||||
logger.info("重置所有持仓状态")
|
||||
@@ -0,0 +1,229 @@
|
||||
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,52 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
实时交易启动脚本 (已重构)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from execution.realtime_trader import RealtimeTrader
|
||||
from core.data_providers 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(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("⚠️ 警告:即将启动实盘交易模式!")
|
||||
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()
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
策略基类
|
||||
"""
|
||||
|
||||
from logger import logger
|
||||
|
||||
class BaseStrategy:
|
||||
"""策略基类 (已重构为依赖注入)"""
|
||||
|
||||
def __init__(self, data_provider, symbol, timeframe):
|
||||
self.data_provider = data_provider
|
||||
self.symbol = symbol
|
||||
self.timeframe = timeframe
|
||||
self.name = self.__class__.__name__
|
||||
|
||||
def _log_signal(self, signal, reason=None):
|
||||
"""记录信号日志"""
|
||||
if signal != 0:
|
||||
direction = "买入" if signal > 0 else "卖出"
|
||||
logger.info(f"{self.name}: 生成 {direction} 信号 (强度: {signal:.2f}), 原因: {reason}")
|
||||
else:
|
||||
logger.debug(f"{self.name}: 无信号, 原因: {reason}")
|
||||
|
||||
def generate_signal(self):
|
||||
"""生成交易信号 - 子类必须实现此方法"""
|
||||
raise NotImplementedError("子类必须实现 generate_signal 方法")
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""回测方法 - 子类必须实现此方法"""
|
||||
raise NotImplementedError("子类必须实现 run_backtest 方法")
|
||||
+20
-38
@@ -1,57 +1,39 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.bollinger_period = 20
|
||||
self.bollinger_std_dev = 2
|
||||
class BollingerStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, period=None, std_dev=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('bollinger', {})
|
||||
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)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算布林带指标
|
||||
"""
|
||||
mean = df['close'].rolling(self.bollinger_period).mean()
|
||||
std = df['close'].rolling(self.bollinger_period).std()
|
||||
df['upper_band'] = mean + self.bollinger_std_dev * std
|
||||
df['lower_band'] = mean - self.bollinger_std_dev * std
|
||||
mean = df['close'].rolling(self.period).mean()
|
||||
std = df['close'].rolling(self.period).std()
|
||||
df['upper_band'] = mean + self.std_dev * std
|
||||
df['lower_band'] = mean - self.std_dev * std
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
布林带策略实盘:
|
||||
当价格跌破下轨买入,涨破上轨卖出。
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.bollinger_period + 30)
|
||||
if rates is None or len(rates) < self.bollinger_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 5)
|
||||
if rates is None or len(rates) < self.period:
|
||||
return 0
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
if df['close'].iloc[-2] < df['lower_band'].iloc[-2]:
|
||||
logger.info(f"价格跌破下轨,产生买入信号: {self.symbol}")
|
||||
if df['close'].iloc[-1] < df['lower_band'].iloc[-1]:
|
||||
return 1
|
||||
elif df['close'].iloc[-2] > df['upper_band'].iloc[-2]:
|
||||
logger.info(f"价格涨破上轨,产生卖出信号: {self.symbol}")
|
||||
elif df['close'].iloc[-1] > df['upper_band'].iloc[-1]:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
布林带策略回测:
|
||||
价格突破下轨买入,突破上轨卖出
|
||||
返回信号序列:1买入,-1卖出,0无操作
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
for i in range(self.bollinger_period, len(df)):
|
||||
if df['close'].iloc[i-1] < df['lower_band'].iloc[i-1]:
|
||||
signals.iat[i] = 1
|
||||
elif df['close'].iloc[i-1] > df['upper_band'].iloc[i-1]:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[df['close'] < df['lower_band']] = 1
|
||||
signals[df['close'] > df['upper_band']] = -1
|
||||
return signals
|
||||
@@ -1,18 +1,16 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from utils import get_rates, has_open_position, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
class DailyBreakoutStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, bars_count=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('daily_breakout', {})
|
||||
self.bars_count = bars_count if bars_count is not None else config.get('bars_count', 1440)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算日内突破指标
|
||||
"""
|
||||
df['time'] = pd.to_datetime(df['time'], unit='s')
|
||||
today = datetime.now().date()
|
||||
day_data = df[df['time'].dt.date == today]
|
||||
@@ -23,11 +21,7 @@ class Strategy:
|
||||
return df, day_high, day_low
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
日内突破策略实盘
|
||||
当价格突破当日最高买入,突破当日最低卖出
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, 1440) # 24 hours * 60 minutes
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.bars_count)
|
||||
if rates is None or len(rates) < 2:
|
||||
return 0
|
||||
df = pd.DataFrame(rates)
|
||||
@@ -36,33 +30,21 @@ class Strategy:
|
||||
if day_high is None or day_low is None:
|
||||
return 0
|
||||
|
||||
if df['close'].iloc[-2] > day_high:
|
||||
logger.info(f"价格突破当日最高,产生买入信号: {self.symbol}")
|
||||
if df['close'].iloc[-1] > day_high:
|
||||
return 1
|
||||
elif df['close'].iloc[-2] < day_low:
|
||||
logger.info(f"价格突破当日最低,产生卖出信号: {self.symbol}")
|
||||
elif df['close'].iloc[-1] < day_low:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
日内突破回测方法
|
||||
计算每个交易日的高低点,突破买卖信号
|
||||
"""
|
||||
df = df.copy()
|
||||
df['time'] = pd.to_datetime(df['time'], unit='s')
|
||||
df['date'] = df['time'].dt.date
|
||||
|
||||
daily_highs = df.groupby('date')['high'].transform('max')
|
||||
daily_lows = df.groupby('date')['low'].transform('max')
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
|
||||
grouped = df.groupby(df['time'].dt.date)
|
||||
|
||||
for date, group in grouped:
|
||||
day_high = group['high'].max()
|
||||
day_low = group['low'].min()
|
||||
for i, row in group.iterrows():
|
||||
if row['close'] > day_high:
|
||||
signals.loc[i] = 1
|
||||
elif row['close'] < day_low:
|
||||
signals.loc[i] = -1
|
||||
|
||||
return signals
|
||||
signals[df['close'] > daily_highs.shift(1)] = 1
|
||||
signals[df['close'] < daily_lows.shift(1)] = -1
|
||||
return signals
|
||||
+17
-39
@@ -1,21 +1,17 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, has_open_position, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.kdj_period = 9
|
||||
class KDJStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, period=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('kdj', {})
|
||||
self.period = period if period is not None else config.get('period', 14)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算KDJ指标
|
||||
"""
|
||||
low_min = df['low'].rolling(self.kdj_period).min()
|
||||
high_max = df['high'].rolling(self.kdj_period).max()
|
||||
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
|
||||
df['k'] = rsv.ewm(com=2).mean()
|
||||
df['d'] = df['k'].ewm(com=2).mean()
|
||||
@@ -23,40 +19,22 @@ class Strategy:
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
KDJ策略实盘
|
||||
K线向上穿越D线(金叉)买入,K线向下穿越D线(死叉)卖出
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.kdj_period + 30)
|
||||
if rates is None or len(rates) < self.kdj_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 5)
|
||||
if rates is None or len(rates) < self.period:
|
||||
return 0
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
# Golden cross
|
||||
if df['k'].iloc[-2] < df['d'].iloc[-2] and df['k'].iloc[-1] > df['d'].iloc[-1]:
|
||||
logger.info(f"KDJ Golden Cross, creating buy signal: {self.symbol}")
|
||||
if df['k'].iloc[-1] > df['d'].iloc[-1] and df['k'].iloc[-2] < df['d'].iloc[-2]:
|
||||
return 1
|
||||
# Dead cross
|
||||
elif df['k'].iloc[-2] > df['d'].iloc[-2] and df['k'].iloc[-1] < df['d'].iloc[-1]:
|
||||
logger.info(f"KDJ Dead Cross, creating sell signal: {self.symbol}")
|
||||
elif df['k'].iloc[-1] < df['d'].iloc[-1] and df['k'].iloc[-2] > df['d'].iloc[-2]:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
KDJ回测方法
|
||||
根据金叉和死叉生成信号
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
for i in range(1, len(df)):
|
||||
# Golden cross
|
||||
if df['k'].iloc[i-1] < df['d'].iloc[i-1] and df['k'].iloc[i] > df['d'].iloc[i]:
|
||||
signals.iat[i] = 1
|
||||
# Dead cross
|
||||
elif df['k'].iloc[i-1] > df['d'].iloc[i-1] and df['k'].iloc[i] < df['d'].iloc[i]:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[(df['k'] > df['d']) & (df['k'].shift(1) < df['d'].shift(1))] = 1
|
||||
signals[(df['k'] < df['d']) & (df['k'].shift(1) > df['d'].shift(1))] = -1
|
||||
return signals
|
||||
+45
-41
@@ -1,56 +1,60 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, close_all, send_order
|
||||
from .base_strategy import BaseStrategy
|
||||
from logger import logger
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.fast_ma_period = 5
|
||||
self.slow_ma_period = 20
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算技术指标
|
||||
"""
|
||||
df['fast_ma'] = df['close'].rolling(self.fast_ma_period).mean()
|
||||
df['slow_ma'] = df['close'].rolling(self.slow_ma_period).mean()
|
||||
return df
|
||||
class MACrossStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, short_window=None, long_window=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('ma_cross', {})
|
||||
self.short_window = short_window if short_window is not None else config.get('short_window', 5)
|
||||
self.long_window = long_window if long_window is not None else config.get('long_window', 20)
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
均线交叉策略实盘:
|
||||
短期均线上穿长期均线买入,下穿卖出。
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.slow_ma_period + 30)
|
||||
if rates is None or len(rates) < self.slow_ma_period:
|
||||
logger.debug(f"--- {self.name} 信号生成开始 ---")
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.long_window + 5) # 获取更多数据以防万一
|
||||
|
||||
if rates is None or len(rates) < self.long_window + 1:
|
||||
logger.debug(f"数据不足或获取失败。需要: {self.long_window + 1}, 实际: {len(rates) if rates is not None else 0}")
|
||||
return 0
|
||||
|
||||
logger.debug(f"获取到 {len(rates)} 条数据")
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
# 计算移动平均线
|
||||
df['short_ma'] = df['close'].rolling(window=self.short_window).mean()
|
||||
df['long_ma'] = df['close'].rolling(window=self.long_window).mean()
|
||||
|
||||
# 提取最后两条数据用于判断
|
||||
latest = df.iloc[-1]
|
||||
previous = df.iloc[-2]
|
||||
|
||||
if df['fast_ma'].iloc[-2] > df['slow_ma'].iloc[-2] and df['fast_ma'].iloc[-3] <= df['slow_ma'].iloc[-3]:
|
||||
logger.info(f"短期均线上穿长期均线,产生买入信号: {self.symbol}")
|
||||
logger.debug(f"最新数据点: Close={latest['close']}, Short MA={latest['short_ma']:.2f}, Long MA={latest['long_ma']:.2f}")
|
||||
logger.debug(f"前一数据点: Close={previous['close']}, Short MA={previous['short_ma']:.2f}, Long MA={previous['long_ma']:.2f}")
|
||||
|
||||
# 判断金叉
|
||||
is_cross_up = latest['short_ma'] > latest['long_ma'] and previous['short_ma'] <= previous['long_ma']
|
||||
logger.debug(f"金叉判断 (is_cross_up): {is_cross_up}")
|
||||
if is_cross_up:
|
||||
logger.info(f"{self.name}: 检测到金叉,生成买入信号")
|
||||
return 1
|
||||
elif df['fast_ma'].iloc[-2] < df['slow_ma'].iloc[-2] and df['fast_ma'].iloc[-3] >= df['slow_ma'].iloc[-3]:
|
||||
logger.info(f"短期均线下穿长期均线,产生卖出信号: {self.symbol}")
|
||||
|
||||
# 判断死叉
|
||||
is_cross_down = latest['short_ma'] < latest['long_ma'] and previous['short_ma'] >= previous['long_ma']
|
||||
logger.debug(f"死叉判断 (is_cross_down): {is_cross_down}")
|
||||
if is_cross_down:
|
||||
logger.info(f"{self.name}: 检测到死叉,生成卖出信号")
|
||||
return -1
|
||||
|
||||
logger.debug(f"--- {self.name} 信号生成结束 (无信号) ---")
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
均线交叉回测:
|
||||
短期均线和长期均线交叉产生信号
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
df['short_ma'] = df['close'].rolling(window=self.short_window).mean()
|
||||
df['long_ma'] = df['close'].rolling(window=self.long_window).mean()
|
||||
signals = pd.Series(0, index=df.index)
|
||||
# 从 slow_ma_period 开始循环,避免早期数据 NaN 问题
|
||||
for i in range(self.slow_ma_period, len(df)):
|
||||
if df['fast_ma'].iloc[i-1] > df['slow_ma'].iloc[i-1] and df['fast_ma'].iloc[i-2] <= df['slow_ma'].iloc[i-2]:
|
||||
signals.iat[i] = 1
|
||||
elif df['fast_ma'].iloc[i-1] < df['slow_ma'].iloc[i-1] and df['fast_ma'].iloc[i-2] >= df['slow_ma'].iloc[i-2]:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[(df['short_ma'] > df['long_ma']) & (df['short_ma'].shift(1) <= df['long_ma'].shift(1))] = 1
|
||||
signals[(df['short_ma'] < df['long_ma']) & (df['short_ma'].shift(1) >= df['long_ma'].shift(1))] = -1
|
||||
return signals
|
||||
+19
-37
@@ -1,58 +1,40 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, has_open_position, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.fast_ema_period = 12
|
||||
self.slow_ema_period = 26
|
||||
self.signal_period = 9
|
||||
class MACDStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, fast_ema=None, slow_ema=None, signal_period=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('macd', {})
|
||||
self.fast_ema = fast_ema if fast_ema is not None else config.get('fast_ema', 12)
|
||||
self.slow_ema = slow_ema if slow_ema is not None else config.get('slow_ema', 26)
|
||||
self.signal_period = signal_period if signal_period is not None else config.get('signal_period', 9)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算MACD指标
|
||||
"""
|
||||
df['exp12'] = df['close'].ewm(span=self.fast_ema_period, adjust=False).mean()
|
||||
df['exp26'] = df['close'].ewm(span=self.slow_ema_period, adjust=False).mean()
|
||||
df['exp12'] = df['close'].ewm(span=self.fast_ema, adjust=False).mean()
|
||||
df['exp26'] = df['close'].ewm(span=self.slow_ema, adjust=False).mean()
|
||||
df['dif'] = df['exp12'] - df['exp26']
|
||||
df['dea'] = df['dif'].ewm(span=self.signal_period, adjust=False).mean()
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
MACD策略实盘
|
||||
DIF线上穿DEA买入,反之卖出
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.slow_ema_period + self.signal_period + 30)
|
||||
if rates is None or len(rates) < self.slow_ema_period + self.signal_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.slow_ema + self.signal_period + 5)
|
||||
if rates is None or len(rates) < self.slow_ema + self.signal_period:
|
||||
return 0
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
if df['dif'].iloc[-2] > df['dea'].iloc[-2] and df['dif'].iloc[-3] <= df['dea'].iloc[-3]:
|
||||
logger.info(f"DIF线上穿DEA,产生买入信号: {self.symbol}")
|
||||
if df['dif'].iloc[-1] > df['dea'].iloc[-1] and df['dif'].iloc[-2] <= df['dea'].iloc[-2]:
|
||||
return 1
|
||||
elif df['dif'].iloc[-2] < df['dea'].iloc[-2] and df['dif'].iloc[-3] >= df['dea'].iloc[-3]:
|
||||
logger.info(f"DIF线下穿DEA,产生卖出信号: {self.symbol}")
|
||||
elif df['dif'].iloc[-1] < df['dea'].iloc[-1] and df['dif'].iloc[-2] >= df['dea'].iloc[-2]:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
MACD回测方法
|
||||
根据DIF和DEA金叉死叉生成信号
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
for i in range(2, len(df)):
|
||||
if df['dif'].iloc[i-1] > df['dea'].iloc[i-1] and df['dif'].iloc[i-2] <= df['dea'].iloc[i-2]:
|
||||
signals.iat[i] = 1
|
||||
elif df['dif'].iloc[i-1] < df['dea'].iloc[i-1] and df['dif'].iloc[i-2] >= df['dea'].iloc[i-2]:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[(df['dif'] > df['dea']) & (df['dif'].shift(1) <= df['dea'].shift(1))] = 1
|
||||
signals[(df['dif'] < df['dea']) & (df['dif'].shift(1) >= df['dea'].shift(1))] = -1
|
||||
return signals
|
||||
@@ -1,57 +1,39 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.mean_reversion_period = 20
|
||||
self.mean_reversion_std_dev = 2
|
||||
class MeanReversionStrategy(BaseStrategy):
|
||||
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)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算均值回归指标
|
||||
"""
|
||||
mean = df['close'].rolling(self.mean_reversion_period).mean()
|
||||
std = df['close'].rolling(self.mean_reversion_period).std()
|
||||
df['upper_band'] = mean + self.mean_reversion_std_dev * std
|
||||
df['lower_band'] = mean - self.mean_reversion_std_dev * std
|
||||
mean = df['close'].rolling(self.period).mean()
|
||||
std = df['close'].rolling(self.period).std()
|
||||
df['upper_band'] = mean + self.std_dev * std
|
||||
df['lower_band'] = mean - self.std_dev * std
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
均值回归策略实盘:
|
||||
当价格超过20日均线正负2个标准差买卖。
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.mean_reversion_period + 30)
|
||||
if rates is None or len(rates) < self.mean_reversion_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 5)
|
||||
if rates is None or len(rates) < self.period:
|
||||
return 0
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
if df['close'].iloc[-2] > df['upper_band'].iloc[-2]:
|
||||
logger.info(f"价格超过上轨,产生卖出信号: {self.symbol}")
|
||||
if df['close'].iloc[-1] > df['upper_band'].iloc[-1]:
|
||||
return -1
|
||||
elif df['close'].iloc[-2] < df['lower_band'].iloc[-2]:
|
||||
logger.info(f"价格低于下轨,产生买入信号: {self.symbol}")
|
||||
elif df['close'].iloc[-1] < df['lower_band'].iloc[-1]:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
均值回归回测:
|
||||
价格突破上下轨卖出/买入
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
for i in range(self.mean_reversion_period, len(df)):
|
||||
if df['close'].iloc[i-1] > df['upper_band'].iloc[i-1]:
|
||||
signals.iat[i] = -1
|
||||
elif df['close'].iloc[i-1] < df['lower_band'].iloc[i-1]:
|
||||
signals.iat[i] = 1
|
||||
return signals
|
||||
signals[df['close'] > df['upper_band']] = -1
|
||||
signals[df['close'] < df['lower_band']] = 1
|
||||
return signals
|
||||
@@ -1,54 +1,36 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, has_open_position, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.breakout_period = 20
|
||||
class MomentumBreakoutStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, 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)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算动量突破指标
|
||||
"""
|
||||
df['high_20'] = df['high'].rolling(self.breakout_period).max()
|
||||
df['low_20'] = df['low'].rolling(self.breakout_period).min()
|
||||
df['high_period'] = df['high'].rolling(self.period).max()
|
||||
df['low_period'] = df['low'].rolling(self.period).min()
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
动量突破策略实盘
|
||||
价格突破过去20根K线最高点买入,突破最低点卖出
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.breakout_period + 30)
|
||||
if rates is None or len(rates) < self.breakout_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 2)
|
||||
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[-2] > df['high_20'].iloc[-3]:
|
||||
logger.info(f"价格突破{self.breakout_period}日最高点,产生买入信号: {self.symbol}")
|
||||
if df['close'].iloc[-1] > df['high_period'].iloc[-2]:
|
||||
return 1
|
||||
elif df['close'].iloc[-2] < df['low_20'].iloc[-3]:
|
||||
logger.info(f"价格突破{self.breakout_period}日最低点,产生卖出信号: {self.symbol}")
|
||||
elif df['close'].iloc[-1] < df['low_period'].iloc[-2]:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
动量突破回测方法
|
||||
过去20根K线最高最低突破生成买卖信号
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
for i in range(self.breakout_period, len(df)):
|
||||
if df['close'].iloc[i-1] > df['high_20'].iloc[i-2]:
|
||||
signals.iat[i] = 1
|
||||
elif df['close'].iloc[i-1] < df['low_20'].iloc[i-2]:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[df['close'] > df['high_period'].shift(1)] = 1
|
||||
signals[df['close'] < df['low_period'].shift(1)] = -1
|
||||
return signals
|
||||
@@ -1,352 +0,0 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from utils import get_rates, has_open_position, get_current_price
|
||||
from logger import logger
|
||||
from config import RISK_CONFIG, SYMBOL
|
||||
|
||||
class Strategy:
|
||||
"""
|
||||
风险管理策略
|
||||
基于市场状态和持仓情况生成风险管理信号
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.symbol = SYMBOL
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.data_count = 100 # 用于分析的数据量
|
||||
|
||||
# 从配置文件读取风险管理参数
|
||||
self.stop_loss_pct = RISK_CONFIG.get("stop_loss_pct", -0.001)
|
||||
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.05)
|
||||
self.take_profit_pct = RISK_CONFIG.get("take_profit_pct", 0.20)
|
||||
self.max_daily_loss = RISK_CONFIG.get("max_daily_loss", -0.10)
|
||||
|
||||
# 策略参数
|
||||
self.volatility_period = 20
|
||||
self.trend_period = 50
|
||||
self.rsi_period = 14
|
||||
|
||||
# 内部状态
|
||||
self.positions = {}
|
||||
self.daily_pnl = 0.0
|
||||
self.last_position_time = None
|
||||
self.min_trade_interval = 5 # 最小交易间隔(分钟)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算技术指标
|
||||
"""
|
||||
# 计算移动平均线
|
||||
df['ma_short'] = df['close'].rolling(10).mean()
|
||||
df['ma_long'] = df['close'].rolling(30).mean()
|
||||
|
||||
# 计算RSI
|
||||
df['rsi'] = self._calculate_rsi(df['close'], self.rsi_period)
|
||||
|
||||
# 计算波动率
|
||||
df['volatility'] = df['close'].rolling(self.volatility_period).std() / df['close'].rolling(self.volatility_period).mean()
|
||||
|
||||
# 计算ATR
|
||||
df['atr'] = self._calculate_atr(df, 14)
|
||||
|
||||
# 计算价格变化
|
||||
df['price_change'] = df['close'].pct_change()
|
||||
|
||||
return df
|
||||
|
||||
def _calculate_rsi(self, prices, period):
|
||||
"""
|
||||
计算RSI指标
|
||||
"""
|
||||
delta = prices.diff()
|
||||
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
||||
rs = gain / loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
return rsi
|
||||
|
||||
def _calculate_atr(self, df, period):
|
||||
"""
|
||||
计算ATR指标
|
||||
"""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
close = df['close']
|
||||
|
||||
tr1 = high - low
|
||||
tr2 = abs(high - close.shift(1))
|
||||
tr3 = abs(low - close.shift(1))
|
||||
|
||||
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
||||
atr = tr.rolling(window=period).mean()
|
||||
|
||||
return atr
|
||||
|
||||
def _get_position_info(self):
|
||||
"""
|
||||
获取当前持仓信息
|
||||
"""
|
||||
return self.positions.get(self.symbol, None)
|
||||
|
||||
def _calculate_position_pnl(self, current_price, position_info):
|
||||
"""
|
||||
计算持仓盈亏
|
||||
"""
|
||||
if not position_info:
|
||||
return 0.0
|
||||
|
||||
entry_price = position_info['entry_price']
|
||||
position_type = position_info['position_type']
|
||||
|
||||
if position_type == 'long':
|
||||
return (current_price - entry_price) / entry_price
|
||||
else:
|
||||
return (entry_price - current_price) / entry_price
|
||||
|
||||
def _check_risk_conditions(self, df):
|
||||
"""
|
||||
检查风险管理条件
|
||||
"""
|
||||
current_price = df['close'].iloc[-1]
|
||||
current_volatility = df['volatility'].iloc[-1]
|
||||
current_rsi = df['rsi'].iloc[-1]
|
||||
current_atr = df['atr'].iloc[-1]
|
||||
|
||||
# 检查是否有持仓
|
||||
position_info = self._get_position_info()
|
||||
|
||||
# 如果没有持仓,检查开仓条件
|
||||
if not position_info:
|
||||
return self._check_entry_conditions(df, current_price, current_volatility, current_rsi, current_atr)
|
||||
|
||||
# 如果有持仓,检查平仓条件
|
||||
else:
|
||||
return self._check_exit_conditions(df, current_price, position_info)
|
||||
|
||||
def _check_entry_conditions(self, df, current_price, volatility, rsi, atr):
|
||||
"""
|
||||
检查开仓条件
|
||||
"""
|
||||
# 检查日亏损限制
|
||||
if self.daily_pnl <= self.max_daily_loss:
|
||||
logger.info(f"日亏损已达{self.daily_pnl:.2%},禁止开仓")
|
||||
return 0
|
||||
|
||||
# 检查最小交易间隔
|
||||
if self.last_position_time:
|
||||
time_diff = (pd.Timestamp.now() - self.last_position_time).total_seconds() / 60
|
||||
if time_diff < self.min_trade_interval:
|
||||
return 0
|
||||
|
||||
# 基于市场状态的开仓条件
|
||||
ma_short = df['ma_short'].iloc[-1]
|
||||
ma_long = df['ma_long'].iloc[-1]
|
||||
|
||||
# 低波动率且趋势明确时开仓
|
||||
if volatility < 0.02: # 低波动率
|
||||
if ma_short > ma_long and rsi < 70: # 上升趋势且未超买
|
||||
logger.info(f"风险管理策略:低波动率上升趋势,买入信号")
|
||||
return 1
|
||||
elif ma_short < ma_long and rsi > 30: # 下降趋势且未超卖
|
||||
logger.info(f"风险管理策略:低波动率下降趋势,卖出信号")
|
||||
return -1
|
||||
|
||||
# 高波动率时等待机会
|
||||
elif volatility > 0.05: # 高波动率
|
||||
if rsi < 30: # 超卖反弹机会
|
||||
logger.info(f"风险管理策略:高波动率超卖反弹,买入信号")
|
||||
return 1
|
||||
elif rsi > 70: # 超买回调机会
|
||||
logger.info(f"风险管理策略:高波动率超买回调,卖出信号")
|
||||
return -1
|
||||
|
||||
return 0
|
||||
|
||||
def _check_exit_conditions(self, df, current_price, position_info):
|
||||
"""
|
||||
检查平仓条件
|
||||
"""
|
||||
position_type = position_info['position_type']
|
||||
entry_price = position_info['entry_price']
|
||||
current_pnl = self._calculate_position_pnl(current_price, position_info)
|
||||
|
||||
# 更新最高利润
|
||||
self.positions[self.symbol]['peak_profit'] = max(
|
||||
position_info.get('peak_profit', 0),
|
||||
current_pnl
|
||||
)
|
||||
peak_profit = self.positions[self.symbol]['peak_profit']
|
||||
|
||||
# 检查止损 - 仅基于买入成本判断亏损,不考虑盈利回撤
|
||||
if current_pnl < 0 and current_pnl <= self.stop_loss_pct:
|
||||
logger.info(f"风险管理策略:止损触发,当前亏损{current_pnl:.2%}")
|
||||
return -1 if position_type == 'long' else 1
|
||||
|
||||
# 检查止盈
|
||||
if current_pnl >= self.take_profit_pct:
|
||||
logger.info(f"风险管理策略:止盈触发,当前盈利{current_pnl:.2%}")
|
||||
return -1 if position_type == 'long' else 1
|
||||
|
||||
# 检查追踪止损
|
||||
if peak_profit > self.min_profit_for_trailing:
|
||||
retracement_from_peak = peak_profit - current_pnl
|
||||
if peak_profit > 0:
|
||||
retracement_pct = retracement_from_peak / peak_profit
|
||||
if retracement_pct >= self.profit_retracement_pct:
|
||||
logger.info(f"风险管理策略:追踪止损触发,最高盈利{peak_profit:.2%},回撤{retracement_pct:.2%}")
|
||||
return -1 if position_type == 'long' else 1
|
||||
|
||||
return 0
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
生成实时交易信号
|
||||
"""
|
||||
# 获取当前价格
|
||||
current_price = get_current_price(self.symbol)
|
||||
if current_price is None:
|
||||
return 0
|
||||
|
||||
# 获取历史数据
|
||||
rates = get_rates(self.symbol, self.timeframe, self.data_count)
|
||||
if rates is None or len(rates) < self.trend_period:
|
||||
return 0
|
||||
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
# 检查风险管理条件
|
||||
signal = self._check_risk_conditions(df)
|
||||
|
||||
return signal
|
||||
|
||||
def generate_signal_with_sync(self, risk_controller):
|
||||
"""
|
||||
生成实时交易信号(与主风险管理器同步)
|
||||
"""
|
||||
# 先同步状态
|
||||
self.sync_with_risk_controller(risk_controller)
|
||||
|
||||
# 然后生成信号
|
||||
return self.generate_signal()
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
回测信号生成
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
|
||||
# 模拟持仓状态
|
||||
positions = {}
|
||||
daily_pnl = 0.0
|
||||
|
||||
for i in range(self.trend_period, len(df)):
|
||||
current_price = df['close'].iloc[i]
|
||||
current_time = df.index[i]
|
||||
|
||||
# 获取当前持仓信息
|
||||
position_info = positions.get(self.symbol)
|
||||
|
||||
if not position_info:
|
||||
# 检查开仓条件
|
||||
if self._should_open_position(df.iloc[:i+1], current_price):
|
||||
# 简化的开仓逻辑
|
||||
ma_short = df['ma_short'].iloc[i]
|
||||
ma_long = df['ma_long'].iloc[i]
|
||||
rsi = df['rsi'].iloc[i]
|
||||
|
||||
if ma_short > ma_long and rsi < 70:
|
||||
signals.iat[i] = 1
|
||||
positions[self.symbol] = {
|
||||
'entry_price': current_price,
|
||||
'position_type': 'long',
|
||||
'peak_profit': 0.0
|
||||
}
|
||||
elif ma_short < ma_long and rsi > 30:
|
||||
signals.iat[i] = -1
|
||||
positions[self.symbol] = {
|
||||
'entry_price': current_price,
|
||||
'position_type': 'short',
|
||||
'peak_profit': 0.0
|
||||
}
|
||||
else:
|
||||
# 检查平仓条件
|
||||
position_type = position_info['position_type']
|
||||
entry_price = position_info['entry_price']
|
||||
current_pnl = self._calculate_position_pnl(current_price, position_info)
|
||||
|
||||
# 更新最高利润
|
||||
position_info['peak_profit'] = max(position_info['peak_profit'], current_pnl)
|
||||
|
||||
# 检查止损止盈 - 止损仅基于买入成本判断亏损
|
||||
if (current_pnl < 0 and current_pnl <= self.stop_loss_pct) or current_pnl >= self.take_profit_pct:
|
||||
signals.iat[i] = -1 if position_type == 'long' else 1
|
||||
positions.pop(self.symbol, None)
|
||||
|
||||
# 更新日内盈亏
|
||||
daily_pnl += current_pnl
|
||||
if daily_pnl <= self.max_daily_loss:
|
||||
break # 日亏损达到限制,停止交易
|
||||
|
||||
return signals
|
||||
|
||||
def _should_open_position(self, df, current_price):
|
||||
"""
|
||||
判断是否应该开仓
|
||||
"""
|
||||
volatility = df['volatility'].iloc[-1]
|
||||
|
||||
# 低波动率且未达到日亏损限制
|
||||
return volatility < 0.02
|
||||
|
||||
def update_position_entry(self, entry_price, position_type="long"):
|
||||
"""
|
||||
更新持仓入场信息
|
||||
"""
|
||||
self.positions[self.symbol] = {
|
||||
'entry_price': entry_price,
|
||||
'position_type': position_type,
|
||||
'peak_profit': 0.0,
|
||||
'entry_time': pd.Timestamp.now()
|
||||
}
|
||||
self.last_position_time = pd.Timestamp.now()
|
||||
|
||||
logger.info(f"风险管理策略记录持仓入场:价格={entry_price:.2f}, 类型={position_type}")
|
||||
|
||||
def close_position(self, current_price):
|
||||
"""
|
||||
平仓
|
||||
"""
|
||||
position_info = self.positions.get(self.symbol)
|
||||
if position_info:
|
||||
pnl = self._calculate_position_pnl(current_price, position_info)
|
||||
self.daily_pnl += pnl
|
||||
|
||||
self.positions.pop(self.symbol, None)
|
||||
logger.info(f"风险管理策略平仓:盈亏{pnl:.2%}")
|
||||
|
||||
def reset_daily_stats(self):
|
||||
"""
|
||||
重置日内统计
|
||||
"""
|
||||
self.daily_pnl = 0.0
|
||||
logger.info("风险管理策略重置日内统计")
|
||||
|
||||
def sync_with_risk_controller(self, risk_controller):
|
||||
"""
|
||||
与主风险管理器同步状态
|
||||
"""
|
||||
# 从主风险管理器获取持仓信息
|
||||
if hasattr(risk_controller, 'position_manager'):
|
||||
position_info = risk_controller.position_manager.get_position_info()
|
||||
if position_info:
|
||||
self.positions[self.symbol] = position_info
|
||||
|
||||
# 同步日内盈亏
|
||||
if hasattr(risk_controller, 'daily_pnl'):
|
||||
self.daily_pnl = risk_controller.daily_pnl
|
||||
+47
-50
@@ -1,65 +1,62 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.rsi_period = 14
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算RSI指标
|
||||
"""
|
||||
delta = df['close'].diff()
|
||||
gain = delta.where(delta > 0, 0).rolling(self.rsi_period).mean()
|
||||
loss = -delta.where(delta < 0, 0).rolling(self.rsi_period).mean()
|
||||
rs = gain / loss
|
||||
df['rsi'] = 100 - (100 / (1 + rs))
|
||||
return df
|
||||
class RSIStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, period=None, overbought=None, oversold=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('rsi', {})
|
||||
self.period = period if period is not None else config.get('period', 14)
|
||||
self.overbought = overbought if overbought is not None else config.get('overbought', 70)
|
||||
self.oversold = oversold if oversold is not None else config.get('oversold', 30)
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
RSI策略实盘
|
||||
RSI上穿超卖线买入,下穿超买线卖出
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.rsi_period + 30)
|
||||
if rates is None or len(rates) < self.rsi_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 10) # Get more data for stability
|
||||
if rates is None or len(rates) < self.period + 1:
|
||||
return 0
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
oversold_level = 30
|
||||
overbought_level = 70
|
||||
# RSI crosses above oversold level
|
||||
if df['rsi'].iloc[-2] < oversold_level and df['rsi'].iloc[-1] > oversold_level:
|
||||
logger.info(f"RSI crosses above {oversold_level}, creating buy signal: {self.symbol}")
|
||||
return 1
|
||||
# RSI crosses below overbought level
|
||||
elif df['rsi'].iloc[-2] > overbought_level and df['rsi'].iloc[-1] < overbought_level:
|
||||
logger.info(f"RSI crosses below {overbought_level}, creating sell signal: {self.symbol}")
|
||||
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
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
|
||||
latest_rsi = rsi.iloc[-1]
|
||||
|
||||
if latest_rsi > self.overbought:
|
||||
return -1
|
||||
|
||||
if latest_rsi < self.oversold:
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
RSI回测方法
|
||||
根据RSI穿越超买超卖线生成信号
|
||||
为RSI策略生成回测信号的向量化方法。
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
# 计算价格变化
|
||||
delta = df['close'].diff()
|
||||
|
||||
# 分别计算上涨和下跌
|
||||
gain = delta.where(delta > 0, 0)
|
||||
loss = -delta.where(delta < 0, 0)
|
||||
|
||||
# 使用指数移动平均(EMA)计算平均增益和损失,这是RSI的标准算法
|
||||
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()
|
||||
|
||||
# 计算RSI
|
||||
rs = avg_gain / avg_loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
|
||||
# 根据超买超卖阈值生成信号
|
||||
signals = pd.Series(0, index=df.index)
|
||||
oversold_level = 30
|
||||
overbought_level = 70
|
||||
for i in range(1, len(df)):
|
||||
# RSI crosses above oversold level
|
||||
if df['rsi'].iloc[i-1] < oversold_level and df['rsi'].iloc[i] > oversold_level:
|
||||
signals.iat[i] = 1
|
||||
# RSI crosses below overbought level
|
||||
elif df['rsi'].iloc[i-1] > overbought_level and df['rsi'].iloc[i] < overbought_level:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[rsi > self.overbought] = -1 # 超买区域,产生卖出信号
|
||||
signals[rsi < self.oversold] = 1 # 超卖区域,产生买入信号
|
||||
|
||||
return signals
|
||||
+17
-35
@@ -1,54 +1,36 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
from utils import get_rates, has_open_position, close_all, send_order
|
||||
from logger import logger
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1
|
||||
self.turtle_period = 20
|
||||
class TurtleStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, period=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('turtle', {})
|
||||
self.period = period if period is not None else config.get('period', 20)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算海龟交易指标
|
||||
"""
|
||||
df['high_20'] = df['high'].rolling(self.turtle_period).max()
|
||||
df['low_20'] = df['low'].rolling(self.turtle_period).min()
|
||||
df['high_period'] = df['high'].rolling(self.period).max()
|
||||
df['low_period'] = df['low'].rolling(self.period).min()
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
海龟交易策略实盘
|
||||
20日最高突破买入,20日最低突破卖出
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.turtle_period + 30)
|
||||
if rates is None or len(rates) < self.turtle_period:
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.period + 2)
|
||||
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[-2] > df['high_20'].iloc[-3]:
|
||||
logger.info(f"价格突破{self.turtle_period}日最高点,产生买入信号: {self.symbol}")
|
||||
if df['close'].iloc[-1] > df['high_period'].iloc[-2]:
|
||||
return 1
|
||||
elif df['close'].iloc[-2] < df['low_20'].iloc[-3]:
|
||||
logger.info(f"价格突破{self.turtle_period}日最低点,产生卖出信号: {self.symbol}")
|
||||
elif df['close'].iloc[-1] < df['low_period'].iloc[-2]:
|
||||
return -1
|
||||
return 0
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
海龟交易回测方法
|
||||
根据20日高低突破生成买卖信号
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
for i in range(self.turtle_period, len(df)):
|
||||
if df['close'].iloc[i-1] > df['high_20'].iloc[i-2]:
|
||||
signals.iat[i] = 1
|
||||
elif df['close'].iloc[i-1] < df['low_20'].iloc[i-2]:
|
||||
signals.iat[i] = -1
|
||||
return signals
|
||||
signals[df['close'] > df['high_period'].shift(1)] = 1
|
||||
signals[df['close'] < df['low_period'].shift(1)] = -1
|
||||
return signals
|
||||
+95
-251
@@ -1,297 +1,141 @@
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from utils import get_rates
|
||||
from logger import logger
|
||||
from config import WAVE_THEORY_CONFIG
|
||||
from .base_strategy import BaseStrategy
|
||||
from config import STRATEGY_CONFIG, DATA_CONFIG
|
||||
|
||||
|
||||
class Strategy:
|
||||
def __init__(self):
|
||||
self.symbol = "XAUUSD"
|
||||
self.timeframe = mt5.TIMEFRAME_M1 # 使用1分钟数据,与其他策略保持一致
|
||||
|
||||
# 从配置文件加载参数
|
||||
config = WAVE_THEORY_CONFIG
|
||||
self.daily_data_count = config.get("daily_data_count", 30)
|
||||
self.ema_short = config.get("ema_short", 5)
|
||||
self.ema_medium = config.get("ema_medium", 13)
|
||||
self.ema_long = config.get("ema_long", 34)
|
||||
self.wave_period = config.get("wave_period", 21)
|
||||
self.range_period = config.get("range_period", 20)
|
||||
self.adx_period = config.get("adx_period", 14)
|
||||
|
||||
# 波浪理论参数
|
||||
class WaveTheoryStrategy(BaseStrategy):
|
||||
def __init__(self, data_provider, symbol, timeframe, m1_bars_count=None, ema_short=None, ema_medium=None, ema_long=None, wave_period=None, range_period=None, adx_period=None, momentum_period=None, range_threshold=None, adx_threshold=None):
|
||||
super().__init__(data_provider, symbol, timeframe)
|
||||
# 从配置中获取参数,如果传入参数则使用传入的参数
|
||||
config = STRATEGY_CONFIG.get('wave_theory', {})
|
||||
self.m1_bars_count = m1_bars_count if m1_bars_count is not None else DATA_CONFIG.get('m1_bars_count', 500)
|
||||
self.ema_short = ema_short if ema_short is not None else config.get('ema_short', 5)
|
||||
self.ema_medium = ema_medium if ema_medium is not None else config.get('ema_medium', 13)
|
||||
self.ema_long = ema_long if ema_long is not None else config.get('ema_long', 34)
|
||||
self.wave_period = wave_period if wave_period is not None else config.get('wave_period', 21)
|
||||
self.range_period = range_period if range_period is not None else config.get('range_period', 20)
|
||||
self.adx_period = adx_period if adx_period is not None else config.get('adx_period', 14)
|
||||
self.retracement_levels = [0.236, 0.382, 0.5, 0.618, 0.786]
|
||||
self.momentum_period = 14
|
||||
|
||||
# 震荡市检测参数(调整为适合1分钟数据)
|
||||
self.range_threshold = 0.005 # 0.5%的价格波动范围,适合1分钟数据
|
||||
self.adx_threshold = 25 # ADX小于25表示震荡市,适合1分钟数据
|
||||
self.momentum_period = momentum_period if momentum_period is not None else config.get('momentum_period', 14)
|
||||
self.range_threshold = range_threshold if range_threshold is not None else config.get('range_threshold', 0.005)
|
||||
self.adx_threshold = adx_threshold if adx_threshold is not None else config.get('adx_threshold', 25)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
"""
|
||||
计算波浪理论相关指标
|
||||
"""
|
||||
# 计算EMA
|
||||
df['ema_short'] = df['close'].ewm(span=self.ema_short).mean()
|
||||
df['ema_medium'] = df['close'].ewm(span=self.ema_medium).mean()
|
||||
df['ema_long'] = df['close'].ewm(span=self.ema_long).mean()
|
||||
|
||||
# 计算动量指标
|
||||
df['momentum'] = df['close'].diff(self.momentum_period) / df['close'].shift(self.momentum_period) * 100
|
||||
|
||||
# 计算波动范围
|
||||
df['high_max'] = df['high'].rolling(self.range_period).max()
|
||||
df['low_min'] = df['low'].rolling(self.range_period).min()
|
||||
df['range_pct'] = (df['high_max'] - df['low_min']) / df['close'] * 100
|
||||
|
||||
# 计算ADX(平均趋向指数)用于判断趋势强度
|
||||
df['adx'] = self._calculate_adx(df)
|
||||
|
||||
# 识别潜在的波浪点
|
||||
df['potential_wave_points'] = self._identify_wave_points(df)
|
||||
|
||||
# 计算斐波那契回撤位
|
||||
df = self._calculate_fibonacci_levels(df)
|
||||
|
||||
return df
|
||||
|
||||
def _calculate_adx(self, df):
|
||||
"""
|
||||
计算ADX指标
|
||||
"""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
close = df['close']
|
||||
|
||||
# 计算真实波幅
|
||||
df['tr'] = pd.concat([
|
||||
high - low,
|
||||
abs(high - close.shift(1)),
|
||||
abs(low - close.shift(1))
|
||||
], axis=1).max(axis=1)
|
||||
|
||||
# 计算方向移动
|
||||
df['tr'] = pd.concat([high - low, abs(high - close.shift(1)), abs(low - close.shift(1))], axis=1).max(axis=1)
|
||||
df['up_move'] = high - high.shift(1)
|
||||
df['down_move'] = low.shift(1) - low
|
||||
|
||||
df['plus_dm'] = np.where((df['up_move'] > df['down_move']) & (df['up_move'] > 0), df['up_move'], 0)
|
||||
df['minus_dm'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0)
|
||||
|
||||
# 计算平滑值
|
||||
df['plus_di'] = 100 * (df['plus_dm'].ewm(span=self.adx_period).mean() / df['tr'].ewm(span=self.adx_period).mean())
|
||||
df['minus_di'] = 100 * (df['minus_dm'].ewm(span=self.adx_period).mean() / df['tr'].ewm(span=self.adx_period).mean())
|
||||
|
||||
# 计算DX
|
||||
df['dx'] = 100 * abs(df['plus_di'] - df['minus_di']) / (df['plus_di'] + df['minus_di'])
|
||||
|
||||
# 计算ADX
|
||||
adx = df['dx'].ewm(span=self.adx_period).mean()
|
||||
|
||||
return adx
|
||||
|
||||
return df['dx'].ewm(span=self.adx_period).mean()
|
||||
|
||||
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)
|
||||
wave_points = pd.Series(0, index=df.index)
|
||||
|
||||
for i in range(self.wave_period, len(df) - self.wave_period):
|
||||
current_high = df['high'].iloc[i]
|
||||
current_low = df['low'].iloc[i]
|
||||
|
||||
# 检查是否为波峰
|
||||
is_peak = (current_high == df['high'].iloc[i-self.wave_period:i+self.wave_period].max())
|
||||
|
||||
# 检查是否为波谷
|
||||
is_trough = (current_low == df['low'].iloc[i-self.wave_period:i+self.wave_period].min())
|
||||
|
||||
if is_peak:
|
||||
wave_points.iloc[i] = 1 # 波峰
|
||||
elif is_trough:
|
||||
wave_points.iloc[i] = -1 # 波谷
|
||||
|
||||
wave_points[df['high'] == df['local_high']] = 1
|
||||
wave_points[df['low'] == df['local_low']] = -1
|
||||
return wave_points
|
||||
|
||||
|
||||
def _calculate_fibonacci_levels(self, df):
|
||||
"""
|
||||
计算斐波那契回撤位
|
||||
"""
|
||||
# 为每个斐波那契级别创建单独的列
|
||||
# 确保fibonacci列存在
|
||||
for level in self.retracement_levels:
|
||||
df[f'fib_{level}'] = None
|
||||
col_name = f'fib_{level}'
|
||||
if col_name not in df.columns:
|
||||
df[col_name] = np.nan
|
||||
|
||||
for i in range(self.wave_period * 2, len(df)):
|
||||
# 寻找最近的波峰和波谷
|
||||
wave_points = df['potential_wave_points'].iloc[:i+1]
|
||||
peaks = wave_points[wave_points == 1]
|
||||
troughs = wave_points[wave_points == -1]
|
||||
last_peak_idx, last_trough_idx = None, None
|
||||
for i in range(1, len(df)):
|
||||
if df['potential_wave_points'].iat[i] == 1:
|
||||
last_peak_idx = df.index[i]
|
||||
elif df['potential_wave_points'].iat[i] == -1:
|
||||
last_trough_idx = df.index[i]
|
||||
|
||||
if len(peaks) > 0 and len(troughs) > 0:
|
||||
last_peak_idx = peaks.index[-1]
|
||||
last_trough_idx = troughs.index[-1]
|
||||
|
||||
if last_peak_idx > last_trough_idx:
|
||||
# 下降趋势,计算回撤位
|
||||
high_price = df['high'].iloc[last_peak_idx]
|
||||
low_price = df['low'].iloc[last_trough_idx]
|
||||
price_range = high_price - low_price
|
||||
|
||||
for level in self.retracement_levels:
|
||||
df.at[i, f'fib_{level}'] = high_price - price_range * level
|
||||
else:
|
||||
# 上升趋势,计算回撤位
|
||||
low_price = df['low'].iloc[last_trough_idx]
|
||||
high_price = df['high'].iloc[last_peak_idx]
|
||||
price_range = high_price - low_price
|
||||
|
||||
for level in self.retracement_levels:
|
||||
df.at[i, f'fib_{level}'] = low_price + price_range * level
|
||||
|
||||
if last_peak_idx is not None and last_trough_idx is not None:
|
||||
try:
|
||||
if last_peak_idx > last_trough_idx:
|
||||
high_price, low_price = df['high'].loc[last_peak_idx], df['low'].loc[last_trough_idx]
|
||||
price_range = high_price - low_price
|
||||
for level in self.retracement_levels:
|
||||
col_name = f'fib_{level}'
|
||||
df.loc[df.index[i], col_name] = high_price - price_range * level
|
||||
else:
|
||||
low_price, high_price = df['low'].loc[last_trough_idx], df['high'].loc[last_peak_idx]
|
||||
price_range = high_price - low_price
|
||||
for level in self.retracement_levels:
|
||||
col_name = f'fib_{level}'
|
||||
df.loc[df.index[i], col_name] = low_price + price_range * level
|
||||
except Exception as e:
|
||||
# 如果计算出错,跳过此位置
|
||||
continue
|
||||
return df
|
||||
|
||||
|
||||
def _is_sideways_market(self, df):
|
||||
"""
|
||||
判断是否为震荡市
|
||||
"""
|
||||
if len(df) < self.range_period:
|
||||
return False
|
||||
|
||||
# 使用ADX判断趋势强度
|
||||
if len(df) < self.range_period: return False
|
||||
adx_value = df['adx'].iloc[-1]
|
||||
is_low_adx = adx_value < self.adx_threshold
|
||||
|
||||
# 使用价格范围判断
|
||||
range_pct = df['range_pct'].iloc[-1]
|
||||
is_tight_range = range_pct < self.range_threshold * 100
|
||||
|
||||
# 结合两个条件
|
||||
return is_low_adx and is_tight_range
|
||||
|
||||
return (adx_value < self.adx_threshold) and (range_pct < self.range_threshold * 100)
|
||||
|
||||
def _calculate_indicators(self, df):
|
||||
df['ema_short'] = df['close'].ewm(span=self.ema_short).mean()
|
||||
df['ema_medium'] = df['close'].ewm(span=self.ema_medium).mean()
|
||||
df['ema_long'] = df['close'].ewm(span=self.ema_long).mean()
|
||||
df['momentum'] = df['close'].diff(self.momentum_period) / df['close'].shift(self.momentum_period) * 100
|
||||
df['high_max'] = df['high'].rolling(self.range_period).max()
|
||||
df['low_min'] = df['low'].rolling(self.range_period).min()
|
||||
df['range_pct'] = (df['high_max'] - df['low_min']) / df['close'] * 100
|
||||
df['adx'] = self._calculate_adx(df)
|
||||
df['potential_wave_points'] = self._identify_wave_points(df)
|
||||
df = self._calculate_fibonacci_levels(df)
|
||||
return df
|
||||
|
||||
def generate_signal(self):
|
||||
"""
|
||||
波浪理论策略实盘信号生成
|
||||
"""
|
||||
rates = get_rates(self.symbol, self.timeframe, self.daily_data_count)
|
||||
rates = self.data_provider.get_historical_data(self.symbol, self.timeframe, self.m1_bars_count)
|
||||
if rates is None or len(rates) < self.wave_period * 3:
|
||||
return 0
|
||||
|
||||
df = pd.DataFrame(rates)
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
# 判断市场状态
|
||||
is_sideways = self._is_sideways_market(df)
|
||||
|
||||
# 获取最近的波浪点
|
||||
recent_wave_points = df['potential_wave_points'].iloc[-self.wave_period:]
|
||||
current_momentum = df['momentum'].iloc[-1]
|
||||
current_price = df['close'].iloc[-1]
|
||||
|
||||
# 震荡市中的波浪理论信号
|
||||
if is_sideways:
|
||||
# 在震荡市中,寻找区间边界的反转机会
|
||||
upper_bound = df['high_max'].iloc[-1]
|
||||
lower_bound = df['low_min'].iloc[-1]
|
||||
|
||||
# 价格接近上边界且有转弱迹象
|
||||
if current_price > upper_bound * 0.98 and current_momentum < 0:
|
||||
logger.info(f"震荡市中价格接近上边界,产生卖出信号: {self.symbol}")
|
||||
return -1
|
||||
|
||||
# 价格接近下边界且有转强迹象
|
||||
elif current_price < lower_bound * 1.02 and current_momentum > 0:
|
||||
logger.info(f"震荡市中价格接近下边界,产生买入信号: {self.symbol}")
|
||||
return 1
|
||||
|
||||
# 趋势市场中的波浪理论信号
|
||||
upper_bound, lower_bound = df['high_max'].iloc[-1], df['low_min'].iloc[-1]
|
||||
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])
|
||||
|
||||
# 上升趋势中的回调买入
|
||||
if ema_alignment and current_momentum > 0:
|
||||
# 检查是否在斐波那契回撤位附近
|
||||
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: # 1%误差范围内
|
||||
logger.info(f"上升趋势中回调至斐波那契61.8%位,产生买入信号: {self.symbol}")
|
||||
return 1
|
||||
|
||||
# 下降趋势中的反弹卖出
|
||||
elif not ema_alignment and current_momentum < 0:
|
||||
# 检查是否在斐波那契回撤位附近
|
||||
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: # 1%误差范围内
|
||||
logger.info(f"下降趋势中反弹至斐波那契61.8%位,产生卖出信号: {self.symbol}")
|
||||
return -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
|
||||
return 0
|
||||
|
||||
|
||||
def run_backtest(self, df):
|
||||
"""
|
||||
波浪理论策略回测
|
||||
"""
|
||||
df = df.copy()
|
||||
df = self._calculate_indicators(df)
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
|
||||
for i in range(self.wave_period * 3, len(df)):
|
||||
current_df = df.iloc[:i+1]
|
||||
|
||||
# 判断市场状态
|
||||
is_sideways = self._is_sideways_market(current_df)
|
||||
|
||||
# 获取当前时刻的数据
|
||||
current_price = current_df['close'].iloc[-1]
|
||||
current_momentum = current_df['momentum'].iloc[-1]
|
||||
|
||||
adx_value = df['adx'].iloc[i]
|
||||
range_pct = df['range_pct'].iloc[i]
|
||||
is_sideways = (adx_value < self.adx_threshold) and (range_pct < self.range_threshold * 100)
|
||||
current_price = df['close'].iloc[i]
|
||||
current_momentum = df['momentum'].iloc[i]
|
||||
if is_sideways:
|
||||
# 震荡市信号
|
||||
upper_bound = current_df['high_max'].iloc[-1]
|
||||
lower_bound = current_df['low_min'].iloc[-1]
|
||||
|
||||
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
|
||||
|
||||
upper_bound, lower_bound = df['high_max'].iloc[i], df['low_min'].iloc[i]
|
||||
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 = (current_df['ema_short'].iloc[-1] > current_df['ema_medium'].iloc[-1] > current_df['ema_long'].iloc[-1])
|
||||
|
||||
if ema_alignment and current_momentum > 0:
|
||||
# 检查斐波那契回撤位
|
||||
if f'fib_0.618' in current_df.columns and not pd.isna(current_df[f'fib_0.618'].iloc[-1]):
|
||||
fib_618 = current_df[f'fib_0.618'].iloc[-1]
|
||||
if abs(current_price - fib_618) / fib_618 < 0.01:
|
||||
signals.iat[i] = 1
|
||||
|
||||
elif not ema_alignment and current_momentum < 0:
|
||||
# 检查斐波那契回撤位
|
||||
if f'fib_0.618' in current_df.columns and not pd.isna(current_df[f'fib_0.618'].iloc[-1]):
|
||||
fib_618 = current_df[f'fib_0.618'].iloc[-1]
|
||||
if abs(current_price - fib_618) / fib_618 < 0.01:
|
||||
signals.iat[i] = -1
|
||||
|
||||
return signals
|
||||
|
||||
def get_market_state(self, df):
|
||||
"""
|
||||
获取当前市场状态信息
|
||||
"""
|
||||
if len(df) < self.wave_period * 3:
|
||||
return {"state": "insufficient_data", "confidence": 0}
|
||||
|
||||
is_sideways = self._is_sideways_market(df)
|
||||
adx_value = df['adx'].iloc[-1]
|
||||
range_pct = df['range_pct'].iloc[-1]
|
||||
|
||||
return {
|
||||
"state": "sideways" if is_sideways else "trending",
|
||||
"confidence": max(0, min(1, (30 - adx_value) / 30)), # ADX越低,震荡市置信度越高
|
||||
"adx": adx_value,
|
||||
"range_pct": range_pct
|
||||
}
|
||||
ema_alignment = (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
|
||||
return signals
|
||||
-193
@@ -1,193 +0,0 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
from logger import logger
|
||||
import json
|
||||
|
||||
class TradeLogger:
|
||||
"""
|
||||
交易日志记录器,跟踪每一笔交易的详情
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.trades = [] # 存储所有交易记录
|
||||
self.current_position = None # 当前持仓信息
|
||||
self.trade_id = 0 # 交易ID计数器
|
||||
|
||||
def open_position(self, symbol, direction, price, timestamp, strategy_signal=""):
|
||||
"""
|
||||
开仓
|
||||
"""
|
||||
if self.current_position is not None:
|
||||
return False
|
||||
|
||||
self.trade_id += 1
|
||||
self.current_position = {
|
||||
'trade_id': self.trade_id,
|
||||
'symbol': symbol,
|
||||
'direction': direction, # 'buy' 或 'sell'
|
||||
'open_price': price,
|
||||
'open_time': timestamp,
|
||||
'strategy_signal': strategy_signal,
|
||||
'status': 'open'
|
||||
}
|
||||
|
||||
logger.info(f"开仓 #{self.trade_id}: {direction} {symbol} @ {price:.2f} 时间: {timestamp}")
|
||||
return True
|
||||
|
||||
def close_position(self, symbol, price, timestamp, close_reason="signal"):
|
||||
"""
|
||||
平仓
|
||||
"""
|
||||
if self.current_position is None:
|
||||
return False
|
||||
|
||||
if self.current_position['symbol'] != symbol:
|
||||
return False
|
||||
|
||||
# 计算盈亏
|
||||
if self.current_position['direction'] == 'buy':
|
||||
profit_loss = price - self.current_position['open_price']
|
||||
profit_loss_pct = profit_loss / self.current_position['open_price'] * 100
|
||||
else: # sell
|
||||
profit_loss = self.current_position['open_price'] - price
|
||||
profit_loss_pct = profit_loss / self.current_position['open_price'] * 100
|
||||
|
||||
# 完成交易记录
|
||||
trade_record = self.current_position.copy()
|
||||
trade_record.update({
|
||||
'close_price': price,
|
||||
'close_time': timestamp,
|
||||
'close_reason': close_reason,
|
||||
'profit_loss': profit_loss,
|
||||
'profit_loss_pct': profit_loss_pct,
|
||||
'status': 'closed'
|
||||
})
|
||||
|
||||
self.trades.append(trade_record)
|
||||
|
||||
logger.info(f"平仓 #{self.trade_id}: {trade_record['direction']} {symbol} @ {price:.2f} | "
|
||||
f"盈亏: {profit_loss:.2f} ({profit_loss_pct:+.2f}%) | 时间: {timestamp}")
|
||||
|
||||
self.current_position = None
|
||||
return True
|
||||
|
||||
def _calculate_profit_loss_pct(self, current_price, position):
|
||||
"""
|
||||
计算当前持仓的盈亏百分比
|
||||
"""
|
||||
if not position:
|
||||
return 0
|
||||
|
||||
if position['direction'] == 'buy':
|
||||
profit_loss = current_price - position['open_price']
|
||||
else: # sell
|
||||
profit_loss = position['open_price'] - current_price
|
||||
|
||||
return profit_loss / position['open_price'] * 100
|
||||
|
||||
def get_trade_summary(self):
|
||||
"""
|
||||
获取交易统计摘要
|
||||
"""
|
||||
if not self.trades:
|
||||
return {
|
||||
'total_trades': 0,
|
||||
'winning_trades': 0,
|
||||
'losing_trades': 0,
|
||||
'win_rate': 0,
|
||||
'total_profit_loss': 0,
|
||||
'avg_profit_loss': 0,
|
||||
'max_profit': 0,
|
||||
'max_loss': 0
|
||||
}
|
||||
|
||||
closed_trades = [t for t in self.trades if t['status'] == 'closed']
|
||||
if not closed_trades:
|
||||
return {
|
||||
'total_trades': len(self.trades),
|
||||
'winning_trades': 0,
|
||||
'losing_trades': 0,
|
||||
'win_rate': 0,
|
||||
'total_profit_loss': 0,
|
||||
'avg_profit_loss': 0,
|
||||
'max_profit': 0,
|
||||
'max_loss': 0
|
||||
}
|
||||
|
||||
profits = [t['profit_loss'] for t in closed_trades]
|
||||
winning_trades = len([p for p in profits if p > 0])
|
||||
losing_trades = len([p for p in profits if p < 0])
|
||||
|
||||
return {
|
||||
'total_trades': len(closed_trades),
|
||||
'winning_trades': winning_trades,
|
||||
'losing_trades': losing_trades,
|
||||
'win_rate': winning_trades / len(closed_trades) * 100 if closed_trades 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 print_all_trades(self):
|
||||
"""
|
||||
打印所有交易记录
|
||||
"""
|
||||
if not self.trades:
|
||||
logger.info("暂无交易记录")
|
||||
return
|
||||
|
||||
logger.info("=" * 80)
|
||||
logger.info("交易记录详情")
|
||||
logger.info("=" * 80)
|
||||
|
||||
for trade in self.trades:
|
||||
if trade['status'] == 'closed':
|
||||
logger.info(f"交易 #{trade['trade_id']}:")
|
||||
logger.info(f" {trade['direction'].upper()} {trade['symbol']}")
|
||||
logger.info(f" 开仓: {trade['open_price']:.2f} @ {trade['open_time']}")
|
||||
logger.info(f" 平仓: {trade['close_price']:.2f} @ {trade['close_time']}")
|
||||
logger.info(f" 盈亏: {trade['profit_loss']:.2f} ({trade['profit_loss_pct']:+.2f}%)")
|
||||
logger.info(f" 原因: {trade['close_reason']}")
|
||||
logger.info("-" * 40)
|
||||
|
||||
# 打印统计摘要
|
||||
summary = self.get_trade_summary()
|
||||
logger.info("交易统计摘要:")
|
||||
logger.info(f" 总交易次数: {summary['total_trades']}")
|
||||
logger.info(f" 盈利次数: {summary['winning_trades']}")
|
||||
logger.info(f" 亏损次数: {summary['losing_trades']}")
|
||||
logger.info(f" 胜率: {summary['win_rate']:.1f}%")
|
||||
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)
|
||||
|
||||
def save_to_csv(self, filename="trades.csv"):
|
||||
"""
|
||||
保存交易记录到CSV文件
|
||||
"""
|
||||
if not self.trades:
|
||||
logger.info("无交易记录可保存")
|
||||
return
|
||||
|
||||
df = pd.DataFrame(self.trades)
|
||||
df.to_csv(filename, index=False, encoding='utf-8-sig')
|
||||
logger.info(f"交易记录已保存到 {filename}")
|
||||
|
||||
def save_to_json(self, filename="trades.json"):
|
||||
"""
|
||||
保存交易记录到JSON文件
|
||||
"""
|
||||
if not self.trades:
|
||||
logger.info("无交易记录可保存")
|
||||
return
|
||||
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.trades, f, ensure_ascii=False, indent=2, default=str)
|
||||
logger.info(f"交易记录已保存到 {filename}")
|
||||
|
||||
# 全局交易日志记录器实例
|
||||
trade_logger = TradeLogger()
|
||||
Reference in New Issue
Block a user