diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..f874bf4 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(pip show:*)", + "Bash(python:*)", + "Bash(myenvScriptspython.exe:*)", + "Bash(\"D:\\tool\\metaTrader5\\mt5_python_ea_suite\\myenv\\Scripts\\python.exe\":*)", + "Bash(D:/tool/metaTrader5/mt5_python_ea_suite/myenv/Scripts/python.exe main.py)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 37bad80..111f298 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /GEMINI.md /logs /myenv -__pycache__/ \ No newline at end of file +__pycache__/ +.json +.csv \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..9c7872c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,112 @@ +# 架构重构说明 + +## 重构概述 + +本次重构将原有的单一策略架构分离为**策略层**和**风险管理层**,实现了更清晰的功能分离和动态权重配置。 + +## 新架构结构 + +### 1. 策略层 (strategies/) +包含纯粹的交易策略,每个策略只负责生成买入/卖出信号: +- `ma_cross.py` - 均线交叉策略 +- `rsi.py` - RSI超买超卖策略 +- `bollinger.py` - 布林带策略 +- `macd.py` - MACD策略 +- `kdj.py` - KDJ策略 +- `turtle.py` - 海龟交易法则 +- `daily_breakout.py` - 日内突破策略 +- `mean_reversion.py` - 均值回归策略 +- `momentum_breakout.py` - 动量突破策略 + +### 2. 风险管理层 (risk_management/) +- `market_state.py` - 市场状态分析器(基于原resilient_trend) +- `position_manager.py` - 仓位管理器(基于原profit_protect) +- `__init__.py` - 风险管理控制器 + +### 3. 动态权重系统 +- `dynamic_weights.py` - 动态权重管理器 + +## 核心改进 + +### 1. 市场状态判断 +- 将`resilient_trend`重构为市场状态分析器 +- 判断市场状态:`uptrend`、`downtrend`、`ranging`、`none` +- 提供置信度评估(0.0-1.0) + +### 2. 动态权重配置 +根据市场状态自动调整策略权重: +- **上升趋势**:侧重趋势跟踪策略(均线交叉、动量突破) +- **下降趋势**:侧重趋势跟踪和反转策略 +- **震荡市场**:侧重均值回归和超买超卖策略 +- **无明确趋势**:使用平衡的权重配置 + +### 3. 综合风险管理 +- **止损机制**:固定止损10% +- **止盈机制**:固定止盈20% + 追踪止损 +- **仓位控制**:最大单笔仓位10% +- **日内限制**:最大日亏损5% + +## 使用方法 + +### 1. 运行回测 +```bash +python main.py +``` +回测会使用当前市场状态对应的权重配置进行测试。 + +### 2. 运行实盘交易 +```bash +# 在main.py中取消注释 +run_realtime() +``` +实盘交易会实时分析市场状态并动态调整权重。 + +### 3. 运行优化 +```bash +# 在main.py中取消注释 +run_optimizer() +``` +优化器会寻找最佳的基础权重配置。 + +## 配置说明 + +### config.py 新配置项 +```python +# 风险管理参数 +RISK_CONFIG = { + "stop_loss_pct": -0.10, # 固定止损:亏损10% + "profit_retracement_pct": 0.30, # 利润回撤30%止盈 + "min_profit_for_trailing": 0.05, # 追踪止损激活阈值:利润5% + "take_profit_pct": 0.20, # 固定止盈:盈利20% + "max_position_size": 0.1, # 最大仓位10% + "max_daily_loss": -0.05, # 最大日亏损5% +} + +# 市场状态分析参数 +MARKET_STATE_CONFIG = { + "trend_period": 50, + "retracement_tolerance": 0.30, +} + +# 默认权重(低置信度时使用) +DEFAULT_WEIGHTS = { + "ma_cross": 1.5, + "rsi": 1.5, + "bollinger": 1.5, + # ... 其他策略 +} +``` + +## 优势 + +1. **更清晰的功能分离**:策略专注于信号生成,风险管理专注于风险控制 +2. **动态适应性**:根据市场状态自动调整策略权重 +3. **更强的风险控制**:多层次的风险管理机制 +4. **更好的可扩展性**:易于添加新策略和风险管理规则 +5. **更稳定的性能**:通过权重分散降低单一策略风险 + +## 兼容性 + +- 保持与现有回测和优化系统的兼容性 +- 策略接口保持不变,现有策略无需修改 +- 配置文件结构更新,但提供了向后兼容 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..786d68e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +此文件为 Claude Code (claude.ai/code) 在此代码库中工作时提供指导。 + +## 项目概述 + +这是一个基于Python的MetaTrader 5智能交易系统(EA)套件,用于量化交易策略。系统通过加权投票和阈值过滤组合多个交易策略,生成稳健的交易决策。 + +## 架构设计 + +### 核心组件 + +- **主入口**: `main.py` 提供三种执行模式: + - `run_backtest()`: 使用当前配置权重进行单次回测 + - `run_optimizer()`: 遗传算法优化寻找最佳权重 + - `run_realtime()`: 与MT5集成的实时交易 + +- **配置文件**: `config.py` 包含: + - `STRATEGIES`: (策略实例, 权重) 元组列表 + - 交易参数(品种、时间周期、初始资金) + +- **回测引擎**: `backtest.py` 处理: + - 历史数据策略执行 + - 使用加权和的信号组合 + - 防止未来函数的收益计算 + +- **遗传优化器**: `optimizer.py`: + - 使用DEAP库进行遗传算法 + - 优化策略权重以获得最大收益 + - 支持并行处理提高性能 + +- **工具函数**: `utils.py` 提供MT5连接管理和交易功能 +- **日志系统**: `logger.py` 处理控制台/文件双日志输出,支持UTF-8编码 + +### 策略架构 + +`strategies/` 目录中的所有策略都遵循一致的模式: + +```python +class Strategy: + def __init__(self): # 初始化参数 + def _calculate_indicators(self, df): # 计算技术指标 + def generate_signal(self): # 实时交易信号生成 + def run_backtest(self, df): # 回测信号生成 +``` + +### 信号组合逻辑 + +系统通过以下方式组合策略信号: +1. 加权求和:`sum(信号 * 权重,针对所有策略)` +2. 信号决策:加权和大于0买入,小于0卖出 +3. 返回用于回测的组合信号序列 + +## 开发命令 + +### 环境设置 +```bash +# 创建并激活虚拟环境 +python -m venv myenv +# Windows +myenv\Scripts\activate +# macOS/Linux +source myenv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 运行模式 +```bash +# 运行单次回测(使用config.py权重) +python main.py + +# 运行遗传优化(寻找最佳权重) +python main.py # 取消注释 run_optimizer() 调用 + +# 运行实时交易(使用config.py权重) +python main.py # 取消注释 run_realtime() 调用 +``` + +## 关键开发模式 + +### 添加新策略 +1. 在 `strategies/` 目录创建新文件 +2. 实现必需的Strategy类方法 +3. 添加导入和策略实例到 `config.py:STRATEGIES` + +### 配置管理 +- 策略权重需要从优化器结果手动更新 +- 所有交易参数集中在 `config.py` 中 + +### 错误处理 +- 策略执行中的全面异常处理 +- 日志文件使用UTF-8编码 +- 操作前进行MT5连接验证 + +### 性能考虑 +- 优化器预加载历史数据避免重复I/O +- 遗传算法启用并行处理 +- 日志器使用单例模式防止重复处理器 + +## 语言要求 + +- **所有开发和文档必须使用中文**(根据GEMINI.md) +- 错误日志在 `logs/error.log` +- 策略日志在 `logs/strategy.log` \ No newline at end of file diff --git a/backtest.py b/backtest.py index ce1b370..dc3497e 100644 --- a/backtest.py +++ b/backtest.py @@ -1,5 +1,8 @@ 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): """ @@ -11,9 +14,15 @@ class BacktestEngine: """ 执行策略的run_backtest,得到信号序列 """ - return strategy.run_backtest(self.df) + 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, buy_threshold, sell_threshold): + def combine_signals(self, signals_list, weights): """ 多策略信号加权合成,并根据阈值生成最终信号 返回合成信号序列 @@ -23,14 +32,21 @@ class BacktestEngine: combined = weighted_signals.sum(axis=1) def apply_threshold(score): - if score >= buy_threshold: + if score > SIGNAL_THRESHOLDS["buy_threshold"]: return 1 - elif score <= sell_threshold: + 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): @@ -43,3 +59,101 @@ class BacktestEngine: 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 diff --git a/backtest_trades.csv b/backtest_trades.csv new file mode 100644 index 0000000..6720e32 --- /dev/null +++ b/backtest_trades.csv @@ -0,0 +1,16 @@ +trade_id,symbol,direction,open_price,open_time,strategy_signal,status,close_price,close_time,close_reason,profit_loss,profit_loss_pct +1,XAUUSD,buy,3325.65,3,signal_1.0,closed,3321.32,45,risk_management_close_long,-4.329999999999927,-0.13020011125644393 +2,XAUUSD,sell,3320.07,46,signal_-1.0,closed,3316.63,48,risk_management_close_long,3.4400000000000546,0.10361227323520451 +3,XAUUSD,sell,3318.83,49,signal_-1.0,closed,3315.46,57,risk_management_close_long,3.369999999999891,0.1015418084083816 +4,XAUUSD,sell,3314.19,58,signal_-1.0,closed,3310.61,62,risk_management_close_long,3.5799999999999272,0.10802036093283508 +5,XAUUSD,sell,3312.83,63,signal_-1.0,closed,3306.49,16527,risk_management_close_long,6.3400000000001455,0.19137716091680362 +6,XAUUSD,sell,3309.13,16528,signal_-1.0,closed,3303.25,16531,risk_management_close_long,5.880000000000109,0.17769020860468188 +7,XAUUSD,sell,3302.25,16532,signal_-1.0,closed,3298.6,19352,risk_management_close_long,3.650000000000091,0.11053069876599564 +8,XAUUSD,sell,3294.11,19353,signal_-1.0,closed,3289.99,19363,risk_management_close_long,4.120000000000346,0.12507171891650082 +9,XAUUSD,sell,3291.61,19364,signal_-1.0,closed,3287.05,19561,risk_management_close_long,4.559999999999945,0.13853403045925688 +10,XAUUSD,sell,3285.18,19562,signal_-1.0,closed,3281.16,19566,risk_management_close_long,4.019999999999982,0.12236772414296879 +11,XAUUSD,sell,3283.41,19567,signal_-1.0,closed,3277.08,19574,risk_management_close_long,6.329999999999927,0.19278737653841363 +12,XAUUSD,sell,3280.13,19575,signal_-1.0,closed,3275.76,19580,risk_management_close_long,4.369999999999891,0.13322642700136553 +13,XAUUSD,sell,3280.44,19581,signal_-1.0,closed,3275.42,19587,risk_management_close_long,5.019999999999982,0.1530282523076167 +14,XAUUSD,sell,3275.99,19588,signal_-1.0,closed,3272.71,19593,risk_management_close_long,3.2799999999997453,0.1001224057460415 +15,XAUUSD,sell,3274.53,19594,signal_-1.0,closed,3271.01,19603,risk_management_close_long,3.519999999999982,0.10749634298662651 diff --git a/backtest_trades.json b/backtest_trades.json new file mode 100644 index 0000000..68af87f --- /dev/null +++ b/backtest_trades.json @@ -0,0 +1,212 @@ +[ + { + "trade_id": 1, + "symbol": "XAUUSD", + "direction": "buy", + "open_price": 3325.65, + "open_time": 3, + "strategy_signal": "signal_1.0", + "status": "closed", + "close_price": 3321.32, + "close_time": 45, + "close_reason": "risk_management_close_long", + "profit_loss": -4.329999999999927, + "profit_loss_pct": -0.13020011125644393 + }, + { + "trade_id": 2, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3320.07, + "open_time": 46, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3316.63, + "close_time": 48, + "close_reason": "risk_management_close_long", + "profit_loss": 3.4400000000000546, + "profit_loss_pct": 0.10361227323520451 + }, + { + "trade_id": 3, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3318.83, + "open_time": 49, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3315.46, + "close_time": 57, + "close_reason": "risk_management_close_long", + "profit_loss": 3.369999999999891, + "profit_loss_pct": 0.1015418084083816 + }, + { + "trade_id": 4, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3314.19, + "open_time": 58, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3310.61, + "close_time": 62, + "close_reason": "risk_management_close_long", + "profit_loss": 3.5799999999999272, + "profit_loss_pct": 0.10802036093283508 + }, + { + "trade_id": 5, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3312.83, + "open_time": 63, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3306.49, + "close_time": 16527, + "close_reason": "risk_management_close_long", + "profit_loss": 6.3400000000001455, + "profit_loss_pct": 0.19137716091680362 + }, + { + "trade_id": 6, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3309.13, + "open_time": 16528, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3303.25, + "close_time": 16531, + "close_reason": "risk_management_close_long", + "profit_loss": 5.880000000000109, + "profit_loss_pct": 0.17769020860468188 + }, + { + "trade_id": 7, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3302.25, + "open_time": 16532, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3298.6, + "close_time": 19352, + "close_reason": "risk_management_close_long", + "profit_loss": 3.650000000000091, + "profit_loss_pct": 0.11053069876599564 + }, + { + "trade_id": 8, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3294.11, + "open_time": 19353, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3289.99, + "close_time": 19363, + "close_reason": "risk_management_close_long", + "profit_loss": 4.120000000000346, + "profit_loss_pct": 0.12507171891650082 + }, + { + "trade_id": 9, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3291.61, + "open_time": 19364, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3287.05, + "close_time": 19561, + "close_reason": "risk_management_close_long", + "profit_loss": 4.559999999999945, + "profit_loss_pct": 0.13853403045925688 + }, + { + "trade_id": 10, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3285.18, + "open_time": 19562, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3281.16, + "close_time": 19566, + "close_reason": "risk_management_close_long", + "profit_loss": 4.019999999999982, + "profit_loss_pct": 0.12236772414296879 + }, + { + "trade_id": 11, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3283.41, + "open_time": 19567, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3277.08, + "close_time": 19574, + "close_reason": "risk_management_close_long", + "profit_loss": 6.329999999999927, + "profit_loss_pct": 0.19278737653841363 + }, + { + "trade_id": 12, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3280.13, + "open_time": 19575, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3275.76, + "close_time": 19580, + "close_reason": "risk_management_close_long", + "profit_loss": 4.369999999999891, + "profit_loss_pct": 0.13322642700136553 + }, + { + "trade_id": 13, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3280.44, + "open_time": 19581, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3275.42, + "close_time": 19587, + "close_reason": "risk_management_close_long", + "profit_loss": 5.019999999999982, + "profit_loss_pct": 0.1530282523076167 + }, + { + "trade_id": 14, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3275.99, + "open_time": 19588, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3272.71, + "close_time": 19593, + "close_reason": "risk_management_close_long", + "profit_loss": 3.2799999999997453, + "profit_loss_pct": 0.1001224057460415 + }, + { + "trade_id": 15, + "symbol": "XAUUSD", + "direction": "sell", + "open_price": 3274.53, + "open_time": 19594, + "strategy_signal": "signal_-1.0", + "status": "closed", + "close_price": 3271.01, + "close_time": 19603, + "close_reason": "risk_management_close_long", + "profit_loss": 3.519999999999982, + "profit_loss_pct": 0.10749634298662651 + } +] \ No newline at end of file diff --git a/config.py b/config.py index a2513f4..08a883a 100644 --- a/config.py +++ b/config.py @@ -1,24 +1,93 @@ -from strategies import ma_cross, rsi, bollinger, mean_reversion, momentum_breakout, macd, kdj, turtle, daily_breakout, profit_protect, resilient_trend - +# 交易配置 SYMBOL = "XAUUSD" INTERVAL = 60 # 秒 INITIAL_CAPITAL = 10000 # 初始资金 -# 策略权重配置 (策略实例, 权重) -STRATEGIES = [ - (ma_cross.Strategy(), 2.28), - (rsi.Strategy(), 1.84), - (bollinger.Strategy(), 1.46), - (mean_reversion.Strategy(), 1.90), - (momentum_breakout.Strategy(), 2.77), - (macd.Strategy(), 1.70), - (kdj.Strategy(), 0.65), - (turtle.Strategy(), 0.68), - (daily_breakout.Strategy(), 0.66), - (profit_protect.Strategy(), 0.57), - (resilient_trend.Strategy(), 1.0), # 新增带容错的趋势策略 -] +# 时间配置 +TIMEFRAME = 1 # M1 (1分钟图) -# 交易信号阈值 -BUY_THRESHOLD = 1.5 -SELL_THRESHOLD = -1.5 +# 回测时间范围 (格式: "YYYY-MM-DD") +# 注意:这些日期需要确保在MT5服务器上有可用数据 +BACKTEST_START_DATE = "2025-05-01" +BACKTEST_END_DATE = "2025-08-01" + +# 优化器时间范围 (格式: "YYYY-MM-DD") +# 注意:确保与回测时间不重叠 +OPTIMIZER_START_DATE = "2025-04-01" +OPTIMIZER_END_DATE = "2025-05-01" + +# 安全设置:如果日期获取失败,自动回退到数据量模式 +USE_DATE_RANGE = False # 设置为False可强制使用数据量模式 + +# 兼容性配置 (如果日期配置不可用,则使用数据量) +BACKTEST_COUNT = 30000 # 回测数据量 +OPTIMIZER_COUNT = 5000 # 优化器数据量 + + + +# 信号阈值配置 +SIGNAL_THRESHOLDS = { + "buy_threshold": 0.5, # 买入信号阈值 + "sell_threshold": -0.5, # 卖出信号阈值 +} + +# 风险管理参数 +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% + "max_position_size": 1, # 最大仓位100% + "max_daily_loss": -0.10, # 最大日亏损10% +} + +# 市场状态分析参数 +MARKET_STATE_CONFIG = { + "trend_period": 50, + "retracement_tolerance": 0.30, + "volume_period": 20, + "volume_ma_period": 10, +} + +# 波浪理论策略配置 +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, +} + +# 市场趋势判断权重配置 +TREND_INDICATOR_WEIGHTS = { + "price_breakout": 0.35, # 价格突破权重 + "volume_confirmation": 0.25, # 成交量确认权重 + "momentum oscillator": 0.20, # 动量震荡指标权重 + "moving_average": 0.20, # 移动平均线权重 +} + +# 趋势判断阈值 +TREND_THRESHOLDS = { + "strong_trend": 0.7, # 强趋势阈值 + "weak_trend": 0.4, # 弱趋势阈值 + "volume_spike": 1.5, # 成交量突增倍数 + "oversold": 30, # 超卖阈值 (RSI) + "overbought": 70, # 超买阈值 (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, # 风险管理策略权重 +} diff --git a/dynamic_weights.py b/dynamic_weights.py new file mode 100644 index 0000000..b9f27c7 --- /dev/null +++ b/dynamic_weights.py @@ -0,0 +1,90 @@ +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()) \ No newline at end of file diff --git a/main.py b/main.py index 8e5719c..f8a7631 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,12 @@ -import importlib import pandas as pd from utils import initialize, shutdown, get_rates, close_all, send_order from backtest import BacktestEngine from logger import logger -from config import INITIAL_CAPITAL, STRATEGIES, BUY_THRESHOLD, SELL_THRESHOLD - +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 @@ -13,78 +15,234 @@ def run_realtime(): 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: + for strat, weight in strategies_with_weights: try: - logger.info(f"执行策略:{strat.__class__.__module__}") - signal = strat.generate_signal() + 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)) - - if weighted_signal_sum >= BUY_THRESHOLD: - logger.info(f"加权信号总和 ({weighted_signal_sum:.2f}) 达到买入阈值 ({BUY_THRESHOLD}),发送买入信号") - close_all("XAUUSD") - send_order("XAUUSD", 'buy') - elif weighted_signal_sum <= SELL_THRESHOLD: - logger.info(f"加权信号总和 ({weighted_signal_sum:.2f}) 达到卖出阈值 ({SELL_THRESHOLD}),发送卖出信号") - close_all("XAUUSD") - send_order("XAUUSD", 'sell') + 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: - logger.info(f"加权信号总和 ({weighted_signal_sum:.2f}) 未达到交易阈值,无操作") + # 信号在中间区域,使用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 - symbol = "XAUUSD" - timeframe = 1 # M1 - count = 50000 - rates = get_rates(symbol, timeframe, count) - shutdown() + # 重置交易日志记录器 + 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: + for strat, weight in strategies_with_weights: try: - logger.info(f"回测策略:{strat.__class__.__module__}") + 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, BUY_THRESHOLD, SELL_THRESHOLD) - cum_ret = engine.calc_returns(combined_signal) + 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") if __name__ == "__main__": # --- 选择运行模式 --- # 1. 运行一次回测 (使用config.py中的默认权重) - # run_backtest() + run_backtest() # 2. 运行实盘交易 (使用config.py中的默认权重) # run_realtime() # 3. 运行遗传算法优化,寻找最佳权重 - run_optimizer() + # run_optimizer() diff --git a/optimizer.py b/optimizer.py index bf8f0aa..9f797ee 100644 --- a/optimizer.py +++ b/optimizer.py @@ -8,12 +8,15 @@ import pandas as pd from utils import initialize, shutdown, get_rates from backtest import BacktestEngine from logger import logger -from config import INITIAL_CAPITAL, STRATEGIES, BUY_THRESHOLD, SELL_THRESHOLD +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 # 1. 定义适应度函数 (已优化) def evaluate_fitness(individual, df_data): """ 评估函数现在接收预先加载的DataFrame作为参数,避免了重复IO。 + 使用新的动态权重架构进行评估。 输入: - individual: 一个代表策略权重的列表。 - df_data: 包含历史K线数据的Pandas DataFrame。 @@ -21,22 +24,33 @@ def evaluate_fitness(individual, df_data): """ weights = individual - # 直接使用传入的df_data,不再需要get_rates + # 使用新的架构进行评估 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_instances = [s for s, w in STRATEGIES] - - for strat in strategy_instances: - signals = engine.run_strategy(strat) - signals_list.append(signals) - - combined_signal = engine.combine_signals(signals_list, weights, BUY_THRESHOLD, SELL_THRESHOLD) + 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]) # 在优化过程中,可以注释掉这行日志以提高速度,因为它会大量输出 - # logger.info(f"评估权重: {[f'{w:.2f}' for w in weights]} -> 最终资金: {final_capital:.2f}") + # logger.info(f"评估权重: {[f'{w:.2f}' for w in weights[:len(signals_list)]]} -> 最终资金: {final_capital:.2f}") return (final_capital,) @@ -52,10 +66,11 @@ def run_optimizer(): logger.error("MT5初始化失败,无法开始优化") return - symbol = "XAUUSD" - timeframe = 1 # M1 - count = 50000 # 使用与回测相同的数据量 - rates = get_rates(symbol, timeframe, count) + # 根据配置选择获取数据的方式 + 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() # 获取数据后即可关闭连接 if rates is None: @@ -72,7 +87,12 @@ def run_optimizer(): toolbox = base.Toolbox() toolbox.register("attr_float", random.uniform, 0.1, 2.0) - num_strategies = len(STRATEGIES) + + # 使用动态权重管理器获取策略数量 + risk_controller = RiskController() + weight_manager = DynamicWeightManager(risk_controller) + num_strategies = len(weight_manager.list_available_strategies()) + toolbox.register("individual", tools.initRepeat, creator.Individual, toolbox.attr_float, n=num_strategies) toolbox.register("population", tools.initRepeat, list, toolbox.individual) diff --git a/risk_management/README.md b/risk_management/README.md new file mode 100644 index 0000000..d8f9491 --- /dev/null +++ b/risk_management/README.md @@ -0,0 +1,7 @@ +# 风险管理模块 + +此模块包含所有风险管理相关的组件: + +- `market_state.py`: 市场状态分析器,基于resilient_trend逻辑 +- `position_manager.py`: 仓位管理器,基于profit_protect逻辑 +- `__init__.py`: 风险管理控制器,统一接口 \ No newline at end of file diff --git a/risk_management/__init__.py b/risk_management/__init__.py new file mode 100644 index 0000000..d8370e3 --- /dev/null +++ b/risk_management/__init__.py @@ -0,0 +1,93 @@ +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("重置所有风险管理状态") \ No newline at end of file diff --git a/risk_management/market_state.py b/risk_management/market_state.py new file mode 100644 index 0000000..4f58835 --- /dev/null +++ b/risk_management/market_state.py @@ -0,0 +1,312 @@ +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') \ No newline at end of file diff --git a/risk_management/position_manager.py b/risk_management/position_manager.py new file mode 100644 index 0000000..35d5fa2 --- /dev/null +++ b/risk_management/position_manager.py @@ -0,0 +1,105 @@ +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("重置所有持仓状态") \ No newline at end of file diff --git a/strategies/kdj.py b/strategies/kdj.py index 323906d..7b6e7a7 100644 --- a/strategies/kdj.py +++ b/strategies/kdj.py @@ -9,9 +9,7 @@ class Strategy: self.symbol = "XAUUSD" self.timeframe = mt5.TIMEFRAME_M1 self.kdj_period = 9 - self.kdj_buy_threshold = 10 - self.kdj_sell_threshold = 90 - + def _calculate_indicators(self, df): """ 计算KDJ指标 @@ -27,7 +25,7 @@ class Strategy: def generate_signal(self): """ KDJ策略实盘 - J值小于10买入,大于90卖出 + 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: @@ -35,26 +33,30 @@ class Strategy: df = pd.DataFrame(rates) df = self._calculate_indicators(df) - if df['j'].iloc[-2] < self.kdj_buy_threshold: - logger.info(f"J值小于{self.kdj_buy_threshold},产生买入信号: {self.symbol}") + # 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}") return 1 - elif df['j'].iloc[-2] > self.kdj_sell_threshold: - logger.info(f"J值大于{self.kdj_sell_threshold},产生卖出信号: {self.symbol}") + # 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}") return -1 return 0 def run_backtest(self, df): """ KDJ回测方法 - 根据J值极端生成信号 + 根据金叉和死叉生成信号 """ df = df.copy() df = self._calculate_indicators(df) signals = pd.Series(0, index=df.index) - for i in range(self.kdj_period, len(df)): - if df['j'].iloc[i-1] < self.kdj_buy_threshold: + 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 - elif df['j'].iloc[i-1] > self.kdj_sell_threshold: + # 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 diff --git a/strategies/profit_protect.py b/strategies/profit_protect.py deleted file mode 100644 index aafba8e..0000000 --- a/strategies/profit_protect.py +++ /dev/null @@ -1,79 +0,0 @@ - -import pandas as pd -from logger import logger - -class Strategy: - def __init__(self): - self.symbol = "XAUUSD" - # --- 策略核心参数 --- - # 固定止损线:亏损10%则卖出 - self.stop_loss_pct = -0.10 - # 利润回撤百分比:从最高利润点回撤30%则卖出 - self.profit_retracement_pct = 0.30 - # 追踪止损的激活阈值:当利润超过5%后,才开始启动追踪止损逻辑 - self.min_profit_for_trailing = 0.05 - - def generate_signal(self): - """ - 此策略为资金管理和退出策略,不产生独立的买入信号。 - 实盘逻辑应与其他策略结合,此处仅为框架完整性。 - """ - logger.warning("ProfitProtect策略是一个退出策略,不应单独用于实盘产生信号。") - return 0 - - def run_backtest(self, df): - """ - 盈利保护策略回测: - - 固定止损:亏损10%卖出。 - - 追踪止损:利润超过5%后启动,从最高利润点回撤30%卖出。 - 为了独立回测,本策略会在一开始买入,然后应用退出逻辑。 - """ - df = df.copy() - signals = pd.Series(0, index=df.index) - - if len(df) < 2: - return signals - - # --- 回测状态变量 --- - position_open = False - entry_price = 0.0 - peak_profit_pct = 0.0 # 记录达到的最高利润百分比 - - for i in range(len(df)): - # 如果没有持仓,就在第一个机会买入(用于独立回测) - if not position_open: - position_open = True - entry_price = df['close'].iloc[i] - signals.iat[i] = 1 # 买入信号 - peak_profit_pct = 0.0 # 重置最高利润 - continue - - # 如果有持仓,则执行退出逻辑 - if position_open: - current_price = df['close'].iloc[i] - current_profit_pct = (current_price - entry_price) / entry_price - - # 1. 更新最高利润点 - peak_profit_pct = max(peak_profit_pct, current_profit_pct) - - # 2. 检查固定止损条件 - if current_profit_pct <= self.stop_loss_pct: - logger.info(f"索引 {i}: 触发固定止损。入场价: {entry_price:.2f}, 当前价: {current_price:.2f}, 亏损: {current_profit_pct:.2%}") - signals.iat[i] = -1 # 卖出信号 - position_open = False # 平仓 - continue - - # 3. 检查追踪止损条件 - # 只有当最高利润超过了激活阈值,才开始计算回撤 - if peak_profit_pct > self.min_profit_for_trailing: - retracement_from_peak = (peak_profit_pct - current_profit_pct) - - # 避免除以零或负数的情况 - if peak_profit_pct > 0: - retracement_pct = retracement_from_peak / peak_profit_pct - if retracement_pct >= self.profit_retracement_pct: - logger.info(f"索引 {i}: 触发追踪止损。最高利润: {peak_profit_pct:.2%}, 当前利润: {current_profit_pct:.2%}, 回撤超过30%") - signals.iat[i] = -1 # 卖出信号 - position_open = False # 平仓 - continue - return signals diff --git a/strategies/resilient_trend.py b/strategies/resilient_trend.py deleted file mode 100644 index 9152ce1..0000000 --- a/strategies/resilient_trend.py +++ /dev/null @@ -1,113 +0,0 @@ - -import pandas as pd -from logger import logger -from utils import get_rates - -class Strategy: - def __init__(self): - self.symbol = "XAUUSD" - # --- 策略核心参数 --- - self.trend_period = 50 - self.retracement_tolerance = 0.30 - - # --- 策略状态变量 --- - self.current_trend = "none" # none, uptrend, downtrend - self.trend_peak = 0.0 # 上升趋势中的最高价 - self.trend_trough = float('inf') # 下降趋势中的最低价 - - def generate_signal(self): - """ - 带状态维护的实盘信号生成方法。 - """ - # 获取足够的数据来计算滚动高低点 - rates = get_rates(self.symbol, 1, self.trend_period + 5) - if rates is None or len(rates) < self.trend_period: - return 0 # 数据不足,不产生信号 - - df = pd.DataFrame(rates) - - # 获取当前价格和用于判断突破的历史高低点 - 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] - - signal = 0 - - # 状态 1: 当前无趋势,等待趋势开始 - if self.current_trend == "none": - if current_price > high_period: - self.current_trend = "uptrend" - self.trend_peak = current_price - signal = 1 - logger.info(f"实盘: 突破进入上升趋势,买入价: {current_price:.2f}") - elif current_price < low_period: - self.current_trend = "downtrend" - self.trend_trough = current_price - signal = -1 - logger.info(f"实盘: 跌破进入下降趋势,卖出价: {current_price:.2f}") - - # 状态 2: 当前处于上升趋势 - elif self.current_trend == "uptrend": - if current_price < self.trend_peak * (1 - self.retracement_tolerance): - logger.info(f"实盘: 上升趋势结束。最高点: {self.trend_peak:.2f}, 当前价: {current_price:.2f}。平仓卖出。") - signal = -1 - self.current_trend = "none" # 重置状态 - else: - self.trend_peak = max(self.trend_peak, current_price) - - # 状态 3: 当前处于下降趋势 - elif self.current_trend == "downtrend": - if current_price > self.trend_trough * (1 + self.retracement_tolerance): - logger.info(f"实盘: 下降趋势结束。最低点: {self.trend_trough:.2f}, 当前价: {current_price:.2f}。平仓买入。") - signal = 1 - self.current_trend = "none" # 重置状态 - else: - self.trend_trough = min(self.trend_trough, current_price) - - return signal - - def run_backtest(self, df): - """ - 带容错的趋势跟踪策略回测: - - 突破N周期高点,进入上升趋势,回撤30%则趋势结束。 - - 跌破N周期低点,进入下降趋势,反弹30%则趋势结束。 - """ - df = df.copy() - signals = pd.Series(0, index=df.index) - - df['high_period'] = df['high'].rolling(self.trend_period).max().shift(1) - df['low_period'] = df['low'].rolling(self.trend_period).min().shift(1) - - # 回测时使用局部变量来管理状态,避免干扰实盘状态 - backtest_trend = "none" - backtest_peak = 0.0 - backtest_trough = float('inf') - - for i in range(self.trend_period, len(df)): - current_price = df['close'].iloc[i] - - if backtest_trend == "none": - if current_price > df['high_period'].iloc[i]: - backtest_trend = "uptrend" - backtest_peak = current_price - signals.iat[i] = 1 - elif current_price < df['low_period'].iloc[i]: - backtest_trend = "downtrend" - backtest_trough = current_price - signals.iat[i] = -1 - - elif backtest_trend == "uptrend": - if current_price < backtest_peak * (1 - self.retracement_tolerance): - signals.iat[i] = -1 - backtest_trend = "none" - else: - backtest_peak = max(backtest_peak, current_price) - - elif backtest_trend == "downtrend": - if current_price > backtest_trough * (1 + self.retracement_tolerance): - signals.iat[i] = 1 - backtest_trend = "none" - else: - backtest_trough = min(backtest_trough, current_price) - - return signals diff --git a/strategies/risk_management.py b/strategies/risk_management.py new file mode 100644 index 0000000..8e5055d --- /dev/null +++ b/strategies/risk_management.py @@ -0,0 +1,352 @@ +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 \ No newline at end of file diff --git a/strategies/rsi.py b/strategies/rsi.py index 469fe54..705d402 100644 --- a/strategies/rsi.py +++ b/strategies/rsi.py @@ -9,8 +9,6 @@ class Strategy: self.symbol = "XAUUSD" self.timeframe = mt5.TIMEFRAME_M1 self.rsi_period = 14 - self.rsi_buy_threshold = 30 - self.rsi_sell_threshold = 70 def _calculate_indicators(self, df): """ @@ -25,8 +23,8 @@ class Strategy: def generate_signal(self): """ - RSI策略实盘: - RSI < 30买入,RSI > 70卖出。 + RSI策略实盘 + RSI上穿超卖线买入,下穿超买线卖出 """ rates = get_rates(self.symbol, self.timeframe, self.rsi_period + 30) if rates is None or len(rates) < self.rsi_period: @@ -34,26 +32,34 @@ class Strategy: df = pd.DataFrame(rates) df = self._calculate_indicators(df) - if df['rsi'].iloc[-2] < self.rsi_buy_threshold: - logger.info(f"RSI小于{self.rsi_buy_threshold},产生买入信号: {self.symbol}") + 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 - elif df['rsi'].iloc[-2] > self.rsi_sell_threshold: - logger.info(f"RSI大于{self.rsi_sell_threshold},产生卖出信号: {self.symbol}") + # 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}") return -1 return 0 def run_backtest(self, df): """ - RSI回测: - RSI < 30买入,RSI > 70卖出。 + RSI回测方法 + 根据RSI穿越超买超卖线生成信号 """ df = df.copy() df = self._calculate_indicators(df) signals = pd.Series(0, index=df.index) - for i in range(self.rsi_period, len(df)): - if df['rsi'].iloc[i-1] < self.rsi_buy_threshold: + 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 - elif df['rsi'].iloc[i-1] > self.rsi_sell_threshold: + # 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 diff --git a/strategies/wave_theory.py b/strategies/wave_theory.py new file mode 100644 index 0000000..e346800 --- /dev/null +++ b/strategies/wave_theory.py @@ -0,0 +1,297 @@ +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 + + +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) + + # 波浪理论参数 + 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分钟数据 + + 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['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 + + def _identify_wave_points(self, df): + """ + 识别潜在的波浪转折点 + """ + 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 # 波谷 + + return wave_points + + def _calculate_fibonacci_levels(self, df): + """ + 计算斐波那契回撤位 + """ + # 为每个斐波那契级别创建单独的列 + for level in self.retracement_levels: + df[f'fib_{level}'] = None + + 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] + + 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 + + return df + + def _is_sideways_market(self, df): + """ + 判断是否为震荡市 + """ + if len(df) < self.range_period: + return False + + # 使用ADX判断趋势强度 + 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 + + def generate_signal(self): + """ + 波浪理论策略实盘信号生成 + """ + rates = get_rates(self.symbol, self.timeframe, self.daily_data_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 + + # 趋势市场中的波浪理论信号 + 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 + + 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] + + 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 + + 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 + } \ No newline at end of file diff --git a/trade_logger.py b/trade_logger.py new file mode 100644 index 0000000..d7f658c --- /dev/null +++ b/trade_logger.py @@ -0,0 +1,193 @@ +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() \ No newline at end of file diff --git a/utils.py b/utils.py index 7d2d4e3..7285176 100644 --- a/utils.py +++ b/utils.py @@ -12,11 +12,55 @@ def initialize(): def shutdown(): mt5.shutdown() -def get_rates(symbol, timeframe, count): - rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, count) - if rates is None: - logger.info(f"获取{symbol}历史数据失败") - return None +def get_rates(symbol, timeframe, count, start_date=None, end_date=None): + """ + 获取历史数据 + 参数: + - symbol: 交易品种 + - timeframe: 时间周期 + - count: 数据量 (当start_date和end_date都为None时使用) + - start_date: 开始日期 (格式: "YYYY-MM-DD" 或 datetime对象) + - end_date: 结束日期 (格式: "YYYY-MM-DD" 或 datetime对象) + """ + if start_date is not None and end_date is not None: + # 使用日期范围获取数据 + import datetime + + # 转换字符串为datetime对象 + if isinstance(start_date, str): + start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d") + if isinstance(end_date, str): + end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d") + + # MetaTrader5需要UTC时间,且copy_rates_range需要timezone-aware的datetime对象 + # 转换为UTC timezone + utc_timezone = datetime.timezone.utc + + start_utc = start_date.replace(tzinfo=utc_timezone) + end_utc = end_date.replace(hour=23, minute=59, second=59, tzinfo=utc_timezone) + + try: + rates = mt5.copy_rates_range(symbol, timeframe, start_utc, end_utc) + 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)}条") + except Exception as e: + logger.error(f"使用日期范围获取数据失败: {e}") + logger.info("回退到使用数据量获取数据") + 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)}条") + 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)}条") + return rates def has_open_position(symbol): @@ -68,3 +112,12 @@ def send_order(symbol, order_type, volume=0.01): logger.error(f"下单失败,retcode={result.retcode}") else: logger.info(f"下单成功: {order_type} {symbol} {volume}") + +def get_current_price(symbol): + """获取当前价格""" + try: + tick = mt5.symbol_info_tick(symbol) + return tick.bid if tick else None + except Exception as e: + logger.error(f"获取当前价格失败: {e}") + return None