diff --git a/.gitignore b/.gitignore index 111f298..e0fb14c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,9 @@ /logs /myenv __pycache__/ -.json -.csv \ No newline at end of file +*.json +*.csv +.claude/ +.claude/** +.claude/settings.local.json +.claude/ \ No newline at end of file diff --git a/README.md b/README.md index 99efd2d..7714d1a 100644 --- a/README.md +++ b/README.md @@ -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` 中,为您的新策略添加一个基础权重。 -完成!您的新策略已经无缝集成到整个系统中,可以立即参与到回测和遗传算法优化中。 +完成!您的新策略已经无缝集成到整个系统中。 \ No newline at end of file diff --git a/backtest.py b/backtest.py deleted file mode 100644 index dc3497e..0000000 --- a/backtest.py +++ /dev/null @@ -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 diff --git a/config.py b/config.py index 08a883a..32192a9 100644 --- a/config.py +++ b/config.py @@ -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, # 中等置信度阈值 +} + diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/data_providers.py b/core/data_providers.py new file mode 100644 index 0000000..2dc575e --- /dev/null +++ b/core/data_providers.py @@ -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 diff --git a/risk_management/README.md b/core/risk/README.md similarity index 100% rename from risk_management/README.md rename to core/risk/README.md diff --git a/core/risk/__init__.py b/core/risk/__init__.py new file mode 100644 index 0000000..e7b7122 --- /dev/null +++ b/core/risk/__init__.py @@ -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) diff --git a/core/risk/market_state.py b/core/risk/market_state.py new file mode 100644 index 0000000..235432b --- /dev/null +++ b/core/risk/market_state.py @@ -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 diff --git a/core/risk/position_manager.py b/core/risk/position_manager.py new file mode 100644 index 0000000..3c4cadd --- /dev/null +++ b/core/risk/position_manager.py @@ -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}") diff --git a/utils.py b/core/utils.py similarity index 56% rename from utils.py rename to core/utils.py index 7285176..7b2bb5f 100644 --- a/utils.py +++ b/core/utils.py @@ -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 diff --git a/demo.txt b/demo.txt new file mode 100644 index 0000000..14ab868 --- /dev/null +++ b/demo.txt @@ -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。 +''' \ No newline at end of file diff --git a/dynamic_weights.py b/dynamic_weights.py deleted file mode 100644 index b9f27c7..0000000 --- a/dynamic_weights.py +++ /dev/null @@ -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()) \ No newline at end of file diff --git a/execution/__init__.py b/execution/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/execution/backtest_engine.py b/execution/backtest_engine.py new file mode 100644 index 0000000..9711ea2 --- /dev/null +++ b/execution/backtest_engine.py @@ -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 diff --git a/execution/dynamic_weights.py b/execution/dynamic_weights.py new file mode 100644 index 0000000..53800b1 --- /dev/null +++ b/execution/dynamic_weights.py @@ -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 + } \ No newline at end of file diff --git a/execution/realtime_trader.py b/execution/realtime_trader.py new file mode 100644 index 0000000..e83591f --- /dev/null +++ b/execution/realtime_trader.py @@ -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) + diff --git a/logger.py b/logger.py index cc24419..4f2749b 100644 --- a/logger.py +++ b/logger.py @@ -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 diff --git a/main.py b/main.py index f8a7631..39a67cf 100644 --- a/main.py +++ b/main.py @@ -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 \ No newline at end of file diff --git a/optimizer.py b/optimizer.py index 9f797ee..0aa242b 100644 --- a/optimizer.py +++ b/optimizer.py @@ -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() \ No newline at end of file diff --git a/params.txt b/params.txt new file mode 100644 index 0000000..5d2dac5 --- /dev/null +++ b/params.txt @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f2676cf..189d6bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ MetaTrader5 pandas deap +tqdm \ No newline at end of file diff --git a/risk_management/__init__.py b/risk_management/__init__.py deleted file mode 100644 index d8370e3..0000000 --- a/risk_management/__init__.py +++ /dev/null @@ -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("重置所有风险管理状态") \ No newline at end of file diff --git a/risk_management/market_state.py b/risk_management/market_state.py deleted file mode 100644 index 4f58835..0000000 --- a/risk_management/market_state.py +++ /dev/null @@ -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') \ No newline at end of file diff --git a/risk_management/position_manager.py b/risk_management/position_manager.py deleted file mode 100644 index 35d5fa2..0000000 --- a/risk_management/position_manager.py +++ /dev/null @@ -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("重置所有持仓状态") \ No newline at end of file diff --git a/start_backtest.py b/start_backtest.py new file mode 100644 index 0000000..3038481 --- /dev/null +++ b/start_backtest.py @@ -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() diff --git a/start_realtime.py b/start_realtime.py new file mode 100644 index 0000000..3f9be52 --- /dev/null +++ b/start_realtime.py @@ -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() \ No newline at end of file diff --git a/strategies/base_strategy.py b/strategies/base_strategy.py new file mode 100644 index 0000000..d227d7e --- /dev/null +++ b/strategies/base_strategy.py @@ -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 方法") diff --git a/strategies/bollinger.py b/strategies/bollinger.py index f949e56..6f2d5a8 100644 --- a/strategies/bollinger.py +++ b/strategies/bollinger.py @@ -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 \ No newline at end of file diff --git a/strategies/daily_breakout.py b/strategies/daily_breakout.py index 62a4e06..5451652 100644 --- a/strategies/daily_breakout.py +++ b/strategies/daily_breakout.py @@ -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 \ No newline at end of file diff --git a/strategies/kdj.py b/strategies/kdj.py index 7b6e7a7..a042d11 100644 --- a/strategies/kdj.py +++ b/strategies/kdj.py @@ -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 \ No newline at end of file diff --git a/strategies/ma_cross.py b/strategies/ma_cross.py index 16a0236..1971138 100644 --- a/strategies/ma_cross.py +++ b/strategies/ma_cross.py @@ -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 \ No newline at end of file diff --git a/strategies/macd.py b/strategies/macd.py index 31e2943..b116250 100644 --- a/strategies/macd.py +++ b/strategies/macd.py @@ -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 \ No newline at end of file diff --git a/strategies/mean_reversion.py b/strategies/mean_reversion.py index 909b23b..458113d 100644 --- a/strategies/mean_reversion.py +++ b/strategies/mean_reversion.py @@ -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 \ No newline at end of file diff --git a/strategies/momentum_breakout.py b/strategies/momentum_breakout.py index 91a6cc0..c63c19a 100644 --- a/strategies/momentum_breakout.py +++ b/strategies/momentum_breakout.py @@ -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 \ No newline at end of file diff --git a/strategies/risk_management.py b/strategies/risk_management.py deleted file mode 100644 index 8e5055d..0000000 --- a/strategies/risk_management.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/strategies/rsi.py b/strategies/rsi.py index 705d402..671f65e 100644 --- a/strategies/rsi.py +++ b/strategies/rsi.py @@ -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 \ No newline at end of file diff --git a/strategies/turtle.py b/strategies/turtle.py index 827b3b7..619fe2b 100644 --- a/strategies/turtle.py +++ b/strategies/turtle.py @@ -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 \ No newline at end of file diff --git a/strategies/wave_theory.py b/strategies/wave_theory.py index e346800..5105c17 100644 --- a/strategies/wave_theory.py +++ b/strategies/wave_theory.py @@ -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 - } \ No newline at end of file + 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 \ No newline at end of file diff --git a/trade_logger.py b/trade_logger.py deleted file mode 100644 index d7f658c..0000000 --- a/trade_logger.py +++ /dev/null @@ -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() \ No newline at end of file