From 4e7c7c7a500e75775b50ba2bff4e8cc70329f56f Mon Sep 17 00:00:00 2001 From: zhangyangbin Date: Mon, 25 May 2026 11:59:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AA=81=E7=A0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IC-15024172/20260516_Breakout.mq5 | 1079 +++++++++++++++++++++++++++++ 1 file changed, 1079 insertions(+) create mode 100644 IC-15024172/20260516_Breakout.mq5 diff --git a/IC-15024172/20260516_Breakout.mq5 b/IC-15024172/20260516_Breakout.mq5 new file mode 100644 index 0000000..cbe0072 --- /dev/null +++ b/IC-15024172/20260516_Breakout.mq5 @@ -0,0 +1,1079 @@ +//+------------------------------------------------------------------+ +//| 20260516_Trade.mq5 | +//| 突破交易策略 - 完整交易版本 | +//+------------------------------------------------------------------+ +#property copyright "Breakout Strategy" +#property version "1.01" +#property strict + +#include + +// 手数计算方式 +enum ENUM_LOT_SIZE_MODE { + LOT_SIZE_FIXED = 0, // 固定手数 + LOT_SIZE_RISK_PERCENT = 1 // 结余风险比例(RiskPercent) +}; + +//+------------------------------------------------------------------+ +//| 输入参数 | +//+------------------------------------------------------------------+ +input group "=== 波段识别参数 ===" +input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // K线周期 +input int InpMAPeriod = 14; // MA周期 +input double InpMinWavePercent = 0.1; // 最小波段阈值百分比(%) +input double InpMaxWavePercent = 10.0; // 最大波段阈值百分比(%) +input double InpPullbackTolerance = 0.0; // 反向突破容忍度(%) 0=不容忍 +input int InpMinWaveBars = 3; // 有效波段最少K线数(含两端极值所在K) + +input group "=== 风险管理参数 ===" +input int InpStopLossPoints = 200; // 止损点数 +input int InpTakeProfitPoints = 300; // 止盈点数 +input bool InpUseTrailingStop = true; // 使用移动止损 + +input group "=== 仓位管理参数 ===" +input ENUM_LOT_SIZE_MODE InpLotSizeMode = LOT_SIZE_RISK_PERCENT; // 手数模式 +input double InpFixedLots = 0.01; // 固定手数(固定模式) +input double InpRiskPercent = 5.0; // 每笔风险占结余%(风险比例模式) +input double InpMaxLots = 99.0; // 单笔最大手数(0=仅受品种限制) +input int InpMaxPositions = 99; // 最大持仓笔数 +input bool InpOnePositionPerDirection = true; // 单方向最多持有一单 + +input group "=== 连续亏损保护 ===" +input int InpConsecutiveLosses = 3; // 连续亏损次数触发冷冻(0=禁用) +input int InpFreezeBarCount = 60; // 冷冻K线根数(0=禁用) + +input group "=== 调试选项 ===" +input bool InpShowDebugInfo = false; // 显示调试信息 +input bool InpShowMarkers = true; // 显示极值点标记 +input int InpMagicNumber = 20260516; // EA魔术号 +input bool InpCloseManualOrders = true; // 禁止手工单(自动平掉) + +//+------------------------------------------------------------------+ +//| 全局变量 | +//+------------------------------------------------------------------+ +int ma_handle; // MA指标句柄 +CTrade trade; // 交易对象 + +// 最新有效波段信息 +struct ValidWaveInfo { + bool exists; // 是否存在有效波段 + double high_price; // 高点价格 + double low_price; // 低点价格 + datetime update_time; // 更新时间 + bool high_used; // 高点是否已使用 + bool low_used; // 低点是否已使用 +}; + +ValidWaveInfo latest_wave; // 最新有效波段 + +double g_sync_wave_high = 0.0; // 已挂单的波段高价(用于检测换波段) +double g_sync_wave_low = 0.0; + +// 连续亏损保护相关变量 +int consecutive_loss_count = 0; // 连续亏损计数器 +datetime freeze_until_time = 0; // 冷冻结束时间(0表示未冷冻) +int freeze_bar_index = 0; // 冷冻起始K线索引 + +// 极值点结构体定义 +struct ExtremePoint { + datetime time; + double price; + int type; + bool is_valid; +}; + +// 函数声明 +void UpdateLatestValidWave(); +int CountBarsBetweenExtremeTimes(const MqlRates &rates[], const datetime t1, const datetime t2); +bool IsValidWaveByBarCount(const MqlRates &rates[], const datetime t1, const datetime t2); +void DrawExtremeMarkers(ExtremePoint &extremes[]); +void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time); +int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]); +void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], + const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]); +void SyncBreakoutPendingOrders(); +void CancelEaPendingOrders(const bool cancel_buy_stop, const bool cancel_sell_stop); +ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type); +bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_price, double &sl, double &tp); +bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const double wave_high, const double wave_low); +double GetStopLossOffset(); +double GetTakeProfitOffset(); +double StopLossAmountFromPositionComment(const string &comment); +double CalculateLotSize(const double wave_high, const double wave_low); +double NormalizeVolumeLots(double lots); +void ManagePositions(); +void CheckTrailingStop(ulong ticket); +void CheckAndCloseManualOrders(); + +//+------------------------------------------------------------------+ +//| Expert initialization function | +//+------------------------------------------------------------------+ +int OnInit() +{ + // 创建MA指标(使用指定的K线周期) + ma_handle = iMA(_Symbol, InpTimeframe, InpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); + if(ma_handle == INVALID_HANDLE) { + Print("创建MA指标失败"); + return(INIT_FAILED); + } + + // 设置交易参数 + trade.SetExpertMagicNumber(InpMagicNumber); + trade.SetDeviationInPoints(10); + trade.SetTypeFilling(ORDER_FILLING_IOC); + + // 初始化最新有效波段 + latest_wave.exists = false; + latest_wave.high_price = 0; + latest_wave.low_price = 0; + latest_wave.update_time = 0; + latest_wave.high_used = false; + latest_wave.low_used = false; + + Print("========================================"); + Print("突破交易策略EA初始化成功"); + Print("品种:", _Symbol); + Print("K线周期:", EnumToString(InpTimeframe)); + Print("MA周期:", InpMAPeriod); + Print("波段阈值范围: ", InpMinWavePercent, "% - ", InpMaxWavePercent, "%"); + Print("有效波段最少K线: ", InpMinWaveBars, " (两极值间含两端,<=1=不限制)"); + if(InpPullbackTolerance > 0) + Print("反向突破容忍度: ", DoubleToString(InpPullbackTolerance, 1), "% (启用)"); + else + Print("反向突破容忍度: 0% (禁用 - 保持原有逻辑)"); + Print("止损:", InpStopLossPoints, "点 | 止盈:", InpTakeProfitPoints, "点"); + Print("移动止损:", (InpUseTrailingStop ? "启用" : "禁用")); + Print("最大持仓笔数:", InpMaxPositions, " 单笔最大手数:", InpMaxLots); + Print("单方向持仓限制:", (InpOnePositionPerDirection ? "启用 (每方向最多1单)" : "禁用")); + if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0) + Print("连续亏损保护: 启用 (", InpConsecutiveLosses, "次亏损→冷冻", InpFreezeBarCount, "根K线)"); + else + Print("连续亏损保护: 禁用"); + if(InpLotSizeMode == LOT_SIZE_RISK_PERCENT) + Print("手数模式: 结余风险比例 ", InpRiskPercent, "% (按止损距离反推手数)"); + else + Print("手数模式: 固定手数 ", InpFixedLots); + Print("开仓方式: 突破挂单 (高点BUY STOP / 低点SELL STOP)"); + Print("禁止手工单:", (InpCloseManualOrders ? "启用 (自动平掉手工单)" : "禁用")); + Print("========================================"); + + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + if(ma_handle != INVALID_HANDLE) + IndicatorRelease(ma_handle); + + // 删除所有标记 + ObjectsDeleteAll(0, "ValidWave_"); + CancelEaPendingOrders(true, true); + + Print("突破交易策略EA已卸载"); +} + +//+------------------------------------------------------------------+ +//| 交易事务处理函数 - 用于检测亏损单 | +//+------------------------------------------------------------------+ +void OnTradeTransaction(const MqlTradeTransaction& trans, + const MqlTradeRequest& request, + const MqlTradeResult& result) +{ + if(trans.type == TRADE_TRANSACTION_DEAL_ADD) { + if(HistoryDealSelect(trans.deal)) { + if(HistoryDealGetString(trans.deal, DEAL_SYMBOL) == _Symbol && + HistoryDealGetInteger(trans.deal, DEAL_MAGIC) == InpMagicNumber && + HistoryDealGetInteger(trans.deal, DEAL_ENTRY) == DEAL_ENTRY_IN) { + const ENUM_DEAL_TYPE deal_type = + (ENUM_DEAL_TYPE)HistoryDealGetInteger(trans.deal, DEAL_TYPE); + if(deal_type == DEAL_TYPE_BUY) + latest_wave.high_used = true; + else if(deal_type == DEAL_TYPE_SELL) + latest_wave.low_used = true; + } + } + } + + // 只在功能启用时处理连续亏损 + if(InpConsecutiveLosses <= 0 || InpFreezeBarCount <= 0) + return; + + // 只处理订单成交事件 + if(trans.type != TRADE_TRANSACTION_DEAL_ADD) + return; + + // 需要先选择历史记录 + if(!HistorySelect(0, TimeCurrent())) + return; + + // 获取最新的Deal + int total_deals = HistoryDealsTotal(); + if(total_deals <= 0) + return; + + ulong deal_ticket = HistoryDealGetTicket(total_deals - 1); + if(deal_ticket == 0) + return; + + // 检查魔术号 + long deal_magic = HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); + if(deal_magic != InpMagicNumber) + return; + + // 检查是否是平仓交易 + ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); + if(entry != DEAL_ENTRY_OUT) + return; + + // 获取交易详情 + double profit = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); + double commission = HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); + double swap = HistoryDealGetDouble(deal_ticket, DEAL_SWAP); + double net_profit = profit + commission + swap; + + // 判断是亏损还是盈利 + if(net_profit < 0) + { + // 亏损:增加计数器 + consecutive_loss_count++; + Print("【连续亏损保护】亏损 ", consecutive_loss_count, "/", InpConsecutiveLosses, + " | 净亏损: $", DoubleToString(net_profit, 2)); + + // 检查是否达到冷冻阈值 + if(consecutive_loss_count >= InpConsecutiveLosses) + { + // 进入冷冻期 + freeze_bar_index = Bars(_Symbol, InpTimeframe) + InpFreezeBarCount; + freeze_until_time = TimeCurrent(); + + Print("!!! 触发交易冷冻 !!! 冷冻", InpFreezeBarCount, "根K线"); + } + } + else if(net_profit > 0) + { + // 盈利:重置计数器 + if(consecutive_loss_count > 0) + { + Print("【连续亏损保护】盈利 - 计数器重置: ", consecutive_loss_count, " → 0 | 净盈利: $", DoubleToString(net_profit, 2)); + consecutive_loss_count = 0; + } + } +} + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 1. 检查并关闭手工单(如果启用) + if(InpCloseManualOrders) + CheckAndCloseManualOrders(); + + // 2. 更新最新有效波段 + UpdateLatestValidWave(); + + // 3. 同步突破挂单(BUY STOP@高 / SELL STOP@低) + SyncBreakoutPendingOrders(); + + // 4. 管理已有持仓 + ManagePositions(); +} + +//+------------------------------------------------------------------+ +//| 更新最新有效波段 | +//+------------------------------------------------------------------+ +int CountBarsBetweenExtremeTimes(const MqlRates &rates[], const datetime t1, const datetime t2) +{ + const datetime t_lo = (t1 <= t2) ? t1 : t2; + const datetime t_hi = (t1 >= t2) ? t1 : t2; + int count = 0; + const int n = ArraySize(rates); + for(int j = 0; j < n; j++) { + if(rates[j].time >= t_lo && rates[j].time <= t_hi) + count++; + } + return count; +} + +bool IsValidWaveByBarCount(const MqlRates &rates[], const datetime t1, const datetime t2) +{ + if(InpMinWaveBars <= 1) + return true; + return (CountBarsBetweenExtremeTimes(rates, t1, t2) >= InpMinWaveBars); +} + +void UpdateLatestValidWave() +{ + int bars = Bars(_Symbol, InpTimeframe); + if(bars < InpMAPeriod + 2) + return; + + // 限制处理的K线数量 + int process_bars = MathMin(bars, 500); + + // 获取价格数据(使用指定的K线周期) + MqlRates rates[]; + ArraySetAsSeries(rates, true); + if(CopyRates(_Symbol, InpTimeframe, 0, process_bars, rates) <= 0) + return; + + // 获取MA数据 + double ma_array[]; + ArraySetAsSeries(ma_array, true); + if(CopyBuffer(ma_handle, 0, 0, process_bars, ma_array) <= 0) + return; + + // 识别突破K线 + int breakout_bars[]; + int breakout_types[]; + ArrayResize(breakout_bars, 0); + ArrayResize(breakout_types, 0); + + for(int i = process_bars - InpMAPeriod - 1; i >= 1; i--) { + int breakout_type = CheckBreakout(i, rates, ma_array); + if(breakout_type != 0) { + int size = ArraySize(breakout_bars); + ArrayResize(breakout_bars, size + 1); + ArrayResize(breakout_types, size + 1); + breakout_bars[size] = i; + breakout_types[size] = breakout_type; + } + } + + // 过滤连续同向突破 + int filtered_bars[]; + int filtered_types[]; + FilterBreakouts(breakout_bars, breakout_types, rates, filtered_bars, filtered_types); + + // 计算极值点(支持反向突破容忍度) + ExtremePoint extremes[]; + ArrayResize(extremes, 0); + + // 如果容忍度为0,使用原有逻辑(相邻突破K线之间的极值) + if(InpPullbackTolerance <= 0.0) + { + for(int i = 0; i < ArraySize(filtered_bars) - 1; i++) { + int current_bar = filtered_bars[i]; + int current_type = filtered_types[i]; + int next_bar = filtered_bars[i + 1]; + + double extreme_price = 0; + datetime extreme_time = 0; + + if(current_type == 1) { + extreme_price = rates[current_bar].high; + extreme_time = rates[current_bar].time; + for(int j = current_bar; j >= next_bar; j--) { + if(rates[j].high > extreme_price) { + extreme_price = rates[j].high; + extreme_time = rates[j].time; + } + } + } else { + extreme_price = rates[current_bar].low; + extreme_time = rates[current_bar].time; + for(int j = current_bar; j >= next_bar; j--) { + if(rates[j].low < extreme_price) { + extreme_price = rates[j].low; + extreme_time = rates[j].time; + } + } + } + + int size = ArraySize(extremes); + ArrayResize(extremes, size + 1); + extremes[size].time = extreme_time; + extremes[size].price = extreme_price; + extremes[size].type = current_type; + extremes[size].is_valid = false; + } + } + else + { + // 容忍模式:跨越反向突破K线计算波段 + for(int i = 0; i < ArraySize(filtered_bars); i++) { + int start_bar = filtered_bars[i]; + int start_type = filtered_types[i]; + + double wave_high = rates[start_bar].high; + double wave_low = rates[start_bar].low; + datetime wave_high_time = rates[start_bar].time; + datetime wave_low_time = rates[start_bar].time; + int end_bar = 0; + bool wave_terminated = false; + + // 向后扫描,直到遇到不可容忍的反向突破 + for(int j = i + 1; j < ArraySize(filtered_bars); j++) { + int current_bar = filtered_bars[j]; + int current_type = filtered_types[j]; + + // 更新波段的高低点 + if(rates[current_bar].high > wave_high) { + wave_high = rates[current_bar].high; + wave_high_time = rates[current_bar].time; + } + if(rates[current_bar].low < wave_low) { + wave_low = rates[current_bar].low; + wave_low_time = rates[current_bar].time; + } + + // 检查中间所有K线的极值 + int prev_bar = (j > 0) ? filtered_bars[j-1] : start_bar; + for(int k = prev_bar; k >= current_bar; k--) { + if(rates[k].high > wave_high) { + wave_high = rates[k].high; + wave_high_time = rates[k].time; + } + if(rates[k].low < wave_low) { + wave_low = rates[k].low; + wave_low_time = rates[k].time; + } + } + + // 如果遇到反向突破K线,检查回撤是否可容忍 + if(current_type != start_type) { + double wave_range = wave_high - wave_low; + double pullback_percent = 0; + + if(start_type == 1) { + // 多头波段遇到空单突破K线 + pullback_percent = ((wave_high - rates[current_bar].close) / wave_range) * 100.0; + } else { + // 空头波段遇到多单突破K线 + pullback_percent = ((rates[current_bar].close - wave_low) / wave_range) * 100.0; + } + + if(pullback_percent > InpPullbackTolerance) { + // 回撤超过容忍度,终止波段 + end_bar = current_bar; + wave_terminated = true; + break; + } + // 否则继续,忽略此反向突破 + } + } + + // 添加极值点 + if(start_type == 1) { + // 多头波段:先低点后高点 + int size = ArraySize(extremes); + ArrayResize(extremes, size + 1); + extremes[size].time = wave_low_time; + extremes[size].price = wave_low; + extremes[size].type = -1; // 低点 + extremes[size].is_valid = false; + + ArrayResize(extremes, size + 2); + extremes[size + 1].time = wave_high_time; + extremes[size + 1].price = wave_high; + extremes[size + 1].type = 1; // 高点 + extremes[size + 1].is_valid = false; + } else { + // 空头波段:先高点后低点 + int size = ArraySize(extremes); + ArrayResize(extremes, size + 1); + extremes[size].time = wave_high_time; + extremes[size].price = wave_high; + extremes[size].type = 1; // 高点 + extremes[size].is_valid = false; + + ArrayResize(extremes, size + 2); + extremes[size + 1].time = wave_low_time; + extremes[size + 1].price = wave_low; + extremes[size + 1].type = -1; // 低点 + extremes[size + 1].is_valid = false; + } + + // 如果波段被终止,跳到终止点继续 + if(wave_terminated) { + // 找到end_bar在filtered_bars中的索引 + for(int k = i + 1; k < ArraySize(filtered_bars); k++) { + if(filtered_bars[k] == end_bar) { + i = k - 1; // -1因为循环会++ + break; + } + } + } else { + // 波段延续到最后 + break; + } + } + } + + // 判断有效波段并标记 + for(int i = 1; i < ArraySize(extremes); i++) { + double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); + double price_diff_points = price_diff / _Point; + + // 计算阈值(百分比模式:以前一个极值点价格为基准计算百分比) + double base_price = extremes[i-1].price; + double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; + double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; + + // 波段必须在最小和最大阈值之间才是有效波段 + if(price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)) { + extremes[i-1].is_valid = true; + extremes[i].is_valid = true; + } + } + + // 绘制所有极值点标记 + if(InpShowMarkers) { + DrawExtremeMarkers(extremes); + } + + // 查找最新的有效波段 + for(int i = ArraySize(extremes) - 1; i >= 1; i--) { + double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); + double price_diff_points = price_diff / _Point; + + // 计算阈值(百分比模式) + double base_price = extremes[i-1].price; + double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; + double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; + + // 波段必须在最小和最大阈值之间才是有效波段 + if(price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)) { + // 找到最新的有效波段 + double high = MathMax(extremes[i].price, extremes[i-1].price); + double low = MathMin(extremes[i].price, extremes[i-1].price); + datetime high_time = (extremes[i].price > extremes[i-1].price) ? extremes[i].time : extremes[i-1].time; + datetime low_time = (extremes[i].price < extremes[i-1].price) ? extremes[i].time : extremes[i-1].time; + + // 检查是否是新的波段 + if(latest_wave.exists == false || + extremes[i].time > latest_wave.update_time || + high != latest_wave.high_price || + low != latest_wave.low_price) { + + latest_wave.exists = true; + latest_wave.high_price = high; + latest_wave.low_price = low; + latest_wave.update_time = extremes[i].time; + latest_wave.high_used = false; // 新波段,重置使用状态 + latest_wave.low_used = false; + + // 绘制最新有效波段 + if(InpShowMarkers) { + DrawLatestValidWave(high, high_time, low, low_time); + } + + if(InpShowDebugInfo) { + Print("更新最新有效波段 - 高:", DoubleToString(high, _Digits), + " 低:", DoubleToString(low, _Digits), + " 价差:", (int)price_diff_points, "点", + " K线数:", CountBarsBetweenExtremeTimes(rates, extremes[i - 1].time, extremes[i].time), + " 阈值范围:", StringFormat("%.2f%%-%.2f%% (%.0f-%.0f点)", + InpMinWavePercent, InpMaxWavePercent, min_threshold, max_threshold)); + } + } + break; + } + } +} + +//+------------------------------------------------------------------+ +//| 绘制所有极值点标记 | +//+------------------------------------------------------------------+ +void DrawExtremeMarkers(ExtremePoint &extremes[]) +{ + // 删除旧的标记 + ObjectsDeleteAll(0, "ValidWave_Extreme_"); + + for(int i = 0; i < ArraySize(extremes); i++) { + string obj_name = "ValidWave_Extreme_" + IntegerToString(i); + + if(extremes[i].type == 1) { + // 高点 - 画下箭头 + ObjectCreate(0, obj_name, OBJ_ARROW, 0, extremes[i].time, extremes[i].price); + ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 234); + ObjectSetInteger(0, obj_name, OBJPROP_COLOR, extremes[i].is_valid ? clrRed : clrDarkRed); + ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, extremes[i].is_valid ? 3 : 1); + ObjectSetInteger(0, obj_name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); + } else { + // 低点 - 画上箭头 + ObjectCreate(0, obj_name, OBJ_ARROW, 0, extremes[i].time, extremes[i].price); + ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 233); + ObjectSetInteger(0, obj_name, OBJPROP_COLOR, extremes[i].is_valid ? clrLime : clrDarkGreen); + ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, extremes[i].is_valid ? 3 : 1); + ObjectSetInteger(0, obj_name, OBJPROP_ANCHOR, ANCHOR_TOP); + } + } +} + +//+------------------------------------------------------------------+ +//| 绘制最新有效波段 | +//+------------------------------------------------------------------+ +void DrawLatestValidWave(double high_price, datetime high_time, double low_price, datetime low_time) +{ + // 删除旧的最新波段标记 + ObjectDelete(0, "ValidWave_Latest_High"); + ObjectDelete(0, "ValidWave_Latest_Low"); + ObjectDelete(0, "ValidWave_Latest_Line"); + + // 标记最新有效波段的高点(更大更亮的箭头) + ObjectCreate(0, "ValidWave_Latest_High", OBJ_ARROW, 0, high_time, high_price); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_ARROWCODE, 234); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_WIDTH, 4); + ObjectSetInteger(0, "ValidWave_Latest_High", OBJPROP_ANCHOR, ANCHOR_BOTTOM); + + // 标记最新有效波段的低点 + ObjectCreate(0, "ValidWave_Latest_Low", OBJ_ARROW, 0, low_time, low_price); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_ARROWCODE, 233); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_WIDTH, 4); + ObjectSetInteger(0, "ValidWave_Latest_Low", OBJPROP_ANCHOR, ANCHOR_TOP); + + // 绘制连接线 + ObjectCreate(0, "ValidWave_Latest_Line", OBJ_TREND, 0, high_time, high_price, low_time, low_price); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_COLOR, clrYellow); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_WIDTH, 2); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_RAY_RIGHT, false); + ObjectSetInteger(0, "ValidWave_Latest_Line", OBJPROP_BACK, true); +} + +//+------------------------------------------------------------------+ +//| 检查是否是突破K线 | +//+------------------------------------------------------------------+ +int CheckBreakout(int index, const MqlRates &rates[], const double &ma[]) +{ + if(rates[index].open < ma[index] && rates[index].close > ma[index]) + return 1; + if(rates[index].open > ma[index] && rates[index].close < ma[index]) + return -1; + return 0; +} + +//+------------------------------------------------------------------+ +//| 过滤连续同向突破 | +//+------------------------------------------------------------------+ +void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], + const MqlRates &rates[], int &filtered_bars[], int &filtered_types[]) +{ + int total = ArraySize(breakout_bars); + ArrayResize(filtered_bars, 0); + ArrayResize(filtered_types, 0); + + for(int i = 0; i < total; i++) { + int current_bar = breakout_bars[i]; + int current_type = breakout_types[i]; + + bool skip = false; + for(int j = i + 1; j < total; j++) { + if(breakout_types[j] != current_type) + break; + + if(current_type == 1) { + if(rates[breakout_bars[j]].low < rates[current_bar].low) { + skip = true; + break; + } + } else { + if(rates[breakout_bars[j]].high > rates[current_bar].high) { + skip = true; + break; + } + } + } + + if(!skip) { + int size = ArraySize(filtered_bars); + ArrayResize(filtered_bars, size + 1); + ArrayResize(filtered_types, size + 1); + filtered_bars[size] = current_bar; + filtered_types[size] = current_type; + } + } +} + +//+------------------------------------------------------------------+ +//| 突破挂单管理 | +//+------------------------------------------------------------------+ +void CancelEaPendingOrders(const bool cancel_buy_stop, const bool cancel_sell_stop) +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) { + const ulong ticket = OrderGetTicket(i); + if(ticket == 0 || !OrderSelect(ticket)) + continue; + if(OrderGetString(ORDER_SYMBOL) != _Symbol) + continue; + if(OrderGetInteger(ORDER_MAGIC) != InpMagicNumber) + continue; + + const ENUM_ORDER_TYPE ot = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); + if(cancel_buy_stop && ot == ORDER_TYPE_BUY_STOP) + trade.OrderDelete(ticket); + else if(cancel_sell_stop && ot == ORDER_TYPE_SELL_STOP) + trade.OrderDelete(ticket); + } +} + +ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type) +{ + for(int i = OrdersTotal() - 1; i >= 0; i--) { + const ulong ticket = OrderGetTicket(i); + if(ticket == 0 || !OrderSelect(ticket)) + continue; + if(OrderGetString(ORDER_SYMBOL) != _Symbol) + continue; + if(OrderGetInteger(ORDER_MAGIC) != InpMagicNumber) + continue; + if((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE) == order_type) + return ticket; + } + return 0; +} + +bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_price, double &sl, double &tp) +{ + const double stop_loss_amount = GetStopLossOffset(); + const double take_profit_amount = GetTakeProfitOffset(); + const int stops_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + const double min_stop_distance = stops_level * _Point; + + sl = 0.0; + tp = 0.0; + + if(pending_type == ORDER_TYPE_BUY_STOP) { + sl = NormalizeDouble(trigger_price - stop_loss_amount, _Digits); + tp = NormalizeDouble(trigger_price + take_profit_amount, _Digits); + if(stops_level > 0) { + if(trigger_price - sl < min_stop_distance) + sl = NormalizeDouble(trigger_price - min_stop_distance, _Digits); + if(tp - trigger_price < min_stop_distance) + tp = NormalizeDouble(trigger_price + min_stop_distance, _Digits); + } + } else if(pending_type == ORDER_TYPE_SELL_STOP) { + sl = NormalizeDouble(trigger_price + stop_loss_amount, _Digits); + tp = NormalizeDouble(trigger_price - take_profit_amount, _Digits); + if(stops_level > 0) { + if(sl - trigger_price < min_stop_distance) + sl = NormalizeDouble(trigger_price + min_stop_distance, _Digits); + if(trigger_price - tp < min_stop_distance) + tp = NormalizeDouble(trigger_price - min_stop_distance, _Digits); + } + } else { + return false; + } + return true; +} + +bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const double wave_high, const double wave_low) +{ + const double lots = CalculateLotSize(wave_high, wave_low); + if(lots <= 0.0) + return false; + + double sl = 0.0, tp = 0.0; + if(!CalcPendingSlTp(pending_type, trigger_price, sl, tp)) + return false; + + const int wave_range_points = (int)MathRound(MathAbs(wave_high - wave_low) / _Point); + const string comment = StringFormat("WR%d", wave_range_points); + + bool result = false; + if(pending_type == ORDER_TYPE_BUY_STOP) + result = trade.BuyStop(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + else if(pending_type == ORDER_TYPE_SELL_STOP) + result = trade.SellStop(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + + if(!result) { + Print("挂单失败 ", EnumToString(pending_type), " 触发价:", trigger_price, + " 错误:", trade.ResultRetcode(), " - ", trade.ResultRetcodeDescription()); + return false; + } + + Print("突破挂单成功 ", EnumToString(pending_type), + " 触发:", trigger_price, " 手数:", lots, + " SL:", sl, " TP:", tp, + " 波段 H:", wave_high, " L:", wave_low); + return true; +} + +void SyncBreakoutPendingOrders() +{ + if(!latest_wave.exists) { + CancelEaPendingOrders(true, true); + g_sync_wave_high = 0.0; + g_sync_wave_low = 0.0; + return; + } + + if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0 && freeze_bar_index > 0) { + const int current_bars = Bars(_Symbol, InpTimeframe); + if(current_bars < freeze_bar_index) { + CancelEaPendingOrders(true, true); + return; + } + if(freeze_bar_index > 0) { + Print("【连续亏损保护】冷冻解除,恢复交易"); + consecutive_loss_count = 0; + freeze_bar_index = 0; + freeze_until_time = 0; + } + } + + if(MathAbs(latest_wave.high_price - g_sync_wave_high) > _Point * 0.5 || + MathAbs(latest_wave.low_price - g_sync_wave_low) > _Point * 0.5) { + CancelEaPendingOrders(true, true); + g_sync_wave_high = latest_wave.high_price; + g_sync_wave_low = latest_wave.low_price; + } + + int total_positions = 0; + int buy_positions = 0; + int sell_positions = 0; + + for(int i = PositionsTotal() - 1; i >= 0; i--) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + total_positions++; + const ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + if(pos_type == POSITION_TYPE_BUY) + buy_positions++; + else if(pos_type == POSITION_TYPE_SELL) + sell_positions++; + } + + if(total_positions >= InpMaxPositions) { + CancelEaPendingOrders(true, true); + if(InpShowDebugInfo) + Print("已达最大持仓笔数,撤销突破挂单: ", total_positions, "/", InpMaxPositions); + return; + } + + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const double wave_high = latest_wave.high_price; + const double wave_low = latest_wave.low_price; + const double lots = CalculateLotSize(wave_high, wave_low); + + // 高点 BUY STOP + if(!latest_wave.high_used && (!InpOnePositionPerDirection || buy_positions < 1)) { + if(ask >= wave_high - _Point * 0.5) { + CancelEaPendingOrders(true, false); + if(InpShowDebugInfo) + Print("价格已越过波段高点,暂不挂BUY STOP Ask:", ask, " 高:", wave_high); + } else { + ulong ticket = FindEaPendingOrder(ORDER_TYPE_BUY_STOP); + if(ticket > 0 && OrderSelect(ticket)) { + const double order_price = OrderGetDouble(ORDER_PRICE_OPEN); + const double order_lots = OrderGetDouble(ORDER_VOLUME_CURRENT); + if(MathAbs(order_price - wave_high) > _Point * 0.5 || + MathAbs(order_lots - lots) > 1e-8) { + trade.OrderDelete(ticket); + ticket = 0; + } + } + if(ticket == 0) + PlaceBreakoutPendingOrder(ORDER_TYPE_BUY_STOP, wave_high, wave_high, wave_low); + } + } else { + CancelEaPendingOrders(true, false); + } + + // 低点 SELL STOP + if(!latest_wave.low_used && (!InpOnePositionPerDirection || sell_positions < 1)) { + if(bid <= wave_low + _Point * 0.5) { + CancelEaPendingOrders(false, true); + if(InpShowDebugInfo) + Print("价格已越过波段低点,暂不挂SELL STOP Bid:", bid, " 低:", wave_low); + } else { + ulong ticket = FindEaPendingOrder(ORDER_TYPE_SELL_STOP); + if(ticket > 0 && OrderSelect(ticket)) { + const double order_price = OrderGetDouble(ORDER_PRICE_OPEN); + const double order_lots = OrderGetDouble(ORDER_VOLUME_CURRENT); + if(MathAbs(order_price - wave_low) > _Point * 0.5 || + MathAbs(order_lots - lots) > 1e-8) { + trade.OrderDelete(ticket); + ticket = 0; + } + } + if(ticket == 0) + PlaceBreakoutPendingOrder(ORDER_TYPE_SELL_STOP, wave_low, wave_high, wave_low); + } + } else { + CancelEaPendingOrders(false, true); + } +} + +//+------------------------------------------------------------------+ +//| 止损/止盈距离(点数) | +//+------------------------------------------------------------------+ +double GetStopLossOffset() +{ + if(InpStopLossPoints <= 0) + return 0.0; + return (double)InpStopLossPoints * _Point; +} + +double GetTakeProfitOffset() +{ + if(InpTakeProfitPoints <= 0) + return 0.0; + return (double)InpTakeProfitPoints * _Point; +} + +double StopLossAmountFromPositionComment(const string &comment) +{ + return GetStopLossOffset(); +} + +//+------------------------------------------------------------------+ +//| 手数规范化 | +//+------------------------------------------------------------------+ +double NormalizeVolumeLots(double lots) +{ + const double min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + const double max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + const double lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + + if(InpMaxLots > 0.0) + lots = MathMin(lots, InpMaxLots); + if(lots < min_lot) + lots = min_lot; + if(lots > max_lot) + lots = max_lot; + if(lot_step > 0.0) + lots = MathFloor(lots / lot_step) * lot_step; + return lots; +} + +//+------------------------------------------------------------------+ +//| 计算开仓手数 | +//+------------------------------------------------------------------+ +double CalculateLotSize(const double wave_high, const double wave_low) +{ + double lots = InpFixedLots; + + if(InpLotSizeMode == LOT_SIZE_RISK_PERCENT && InpRiskPercent > 0.0) { + const double stop_loss_dist = GetStopLossOffset(); + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + const double risk_money = balance * InpRiskPercent / 100.0; + + const double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + const double tick_value = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + if(stop_loss_dist > 0.0 && tick_size > 0.0 && tick_value > 0.0 && risk_money > 0.0) { + const double loss_per_lot = (stop_loss_dist / tick_size) * tick_value; + if(loss_per_lot > 0.0) + lots = risk_money / loss_per_lot; + } + } + + return NormalizeVolumeLots(lots); +} + +//+------------------------------------------------------------------+ +//| 管理持仓 | +//+------------------------------------------------------------------+ +void ManagePositions() +{ + for(int i = PositionsTotal() - 1; i >= 0; i--) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + + if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) + continue; + + // 检查移动止损 + ulong ticket = PositionGetTicket(i); + CheckTrailingStop(ticket); + } +} + +//+------------------------------------------------------------------+ +//| 检查移动止损 | +//+------------------------------------------------------------------+ +void CheckTrailingStop(ulong ticket) +{ + // 如果未开启移动止损,直接返回 + if(!InpUseTrailingStop) + return; + + if(!PositionSelectByTicket(ticket)) + return; + + double open_price = PositionGetDouble(POSITION_PRICE_OPEN); + double current_sl = PositionGetDouble(POSITION_SL); + ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); + double current_price = (pos_type == POSITION_TYPE_BUY) ? + SymbolInfoDouble(_Symbol, SYMBOL_BID) : + SymbolInfoDouble(_Symbol, SYMBOL_ASK); + + // 计算止损金额(有效波段区间 × 比例,备注中 WR 存区间点数) + const string comment = PositionGetString(POSITION_COMMENT); + const double stop_loss_amount = StopLossAmountFromPositionComment(comment); + + // 计算浮盈 + double profit_amount = 0; + if(pos_type == POSITION_TYPE_BUY) { + profit_amount = current_price - open_price; + } else { + profit_amount = open_price - current_price; + } + + // 浮盈达到止损金额,移动止损至成本价 + if(profit_amount >= stop_loss_amount) { + double new_sl = open_price; + + // 检查是否需要更新 + bool need_update = false; + if(pos_type == POSITION_TYPE_BUY && (current_sl < new_sl || current_sl == 0)) { + need_update = true; + } else if(pos_type == POSITION_TYPE_SELL && (current_sl > new_sl || current_sl == 0)) { + need_update = true; + } + + if(need_update) { + double tp = PositionGetDouble(POSITION_TP); + if(trade.PositionModify(ticket, new_sl, tp)) { + Print("移动止损至成本价 - Ticket:", ticket, " 新止损:", new_sl); + } + } + } +} + +//+------------------------------------------------------------------+ +//| 检查并关闭手工单 | +//+------------------------------------------------------------------+ +void CheckAndCloseManualOrders() +{ + for(int i = PositionsTotal() - 1; i >= 0; i--) { + if(!PositionSelectByTicket(PositionGetTicket(i))) + continue; + + // 只处理本品种 + if(PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + + // 检查magic number:0表示手工单 + long magic = PositionGetInteger(POSITION_MAGIC); + if(magic == 0) { + ulong ticket = PositionGetTicket(i); + + // 平仓手工单 + if(trade.PositionClose(ticket)) { + Print("【禁止手工单】已平掉手工单 - Ticket:", ticket, + " 类型:", (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? "多单" : "空单")); + } else { + Print("【禁止手工单】平仓失败 - Ticket:", ticket, " 错误:", GetLastError()); + } + } + } +}