基本完毕

This commit is contained in:
songkunling
2025-08-14 10:13:04 +08:00
parent 21ce1831ec
commit 769729e610
40 changed files with 3177 additions and 2372 deletions
+6 -2
View File
@@ -2,5 +2,9 @@
/logs
/myenv
__pycache__/
.json
.csv
*.json
*.csv
.claude/
.claude/**
.claude/settings.local.json
.claude/
+60 -60
View File
@@ -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
View File
@@ -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
+230 -44
View File
@@ -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, # 中等置信度阈值
}
View File
+373
View File
@@ -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
+32
View File
@@ -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)
+151
View File
@@ -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
+429
View File
@@ -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}")
+85 -6
View File
@@ -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
+97
View File
@@ -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。
'''
-90
View File
@@ -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())
View File
+95
View File
@@ -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
+67
View File
@@ -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
}
+141
View File
@@ -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)
+17 -9
View File
@@ -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
+11 -237
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,4 @@
MetaTrader5
pandas
deap
tqdm
-93
View File
@@ -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("重置所有风险管理状态")
-312
View File
@@ -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')
-105
View File
@@ -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("重置所有持仓状态")
+229
View File
@@ -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()
+52
View File
@@ -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()
+32
View File
@@ -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
View File
@@ -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
+18 -36
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+20 -38
View File
@@ -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
+17 -35
View File
@@ -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
-352
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()