diff --git a/20260516-突破策略/20260516_Breakout.mq5 b/20260516-突破策略/20260516_Breakout.mq5 index cbe0072..e7f042c 100644 --- a/20260516-突破策略/20260516_Breakout.mq5 +++ b/20260516-突破策略/20260516_Breakout.mq5 @@ -3,7 +3,7 @@ //| 突破交易策略 - 完整交易版本 | //+------------------------------------------------------------------+ #property copyright "Breakout Strategy" -#property version "1.01" +#property version "1.03" #property strict #include @@ -14,6 +14,12 @@ enum ENUM_LOT_SIZE_MODE { LOT_SIZE_RISK_PERCENT = 1 // 结余风险比例(RiskPercent) }; +// 破高/破低开仓方向 +enum ENUM_BREAKOUT_DIRECTION { + BREAKOUT_DIR_FOLLOW = 0, // 顺势(破高多/破低空) + BREAKOUT_DIR_REVERSE = 1 // 反向(破高空/破低多) +}; + //+------------------------------------------------------------------+ //| 输入参数 | //+------------------------------------------------------------------+ @@ -25,6 +31,11 @@ input double InpMaxWavePercent = 10.0; // 最大波段阈值百分比(% input double InpPullbackTolerance = 0.0; // 反向突破容忍度(%) 0=不容忍 input int InpMinWaveBars = 3; // 有效波段最少K线数(含两端极值所在K) +input group "=== 突破交易参数 ===" +input ENUM_BREAKOUT_DIRECTION InpBreakoutDirection = BREAKOUT_DIR_FOLLOW; // 开仓方向 +input bool InpEnablePullbackReverse = false; // 启用突破回落反向 +input int InpPullbackReverseWatchBars = 3; // 回落监测K线数(突破K+后2根=3) + input group "=== 风险管理参数 ===" input int InpStopLossPoints = 200; // 止损点数 input int InpTakeProfitPoints = 300; // 止盈点数 @@ -60,12 +71,27 @@ struct ValidWaveInfo { double high_price; // 高点价格 double low_price; // 低点价格 datetime update_time; // 更新时间 - bool high_used; // 高点是否已使用 - bool low_used; // 低点是否已使用 + bool high_used; // 高点-突破挂单已用(每极值限1次) + bool low_used; // 低点-突破挂单已用(每极值限1次) + bool high_pullback_reverse_used; // 高点-回落反向已用(每极值限1次) + bool low_pullback_reverse_used; // 低点-回落反向已用(每极值限1次) +}; + +struct PullbackWatchState { + bool active; + double extreme_price; + int bars_checked; }; ValidWaveInfo latest_wave; // 最新有效波段 +PullbackWatchState g_high_pullback_watch; +PullbackWatchState g_low_pullback_watch; +datetime g_pullback_bar_time_high = 0; +bool g_pullback_opened_on_bar_high = false; +datetime g_pullback_bar_time_low = 0; +bool g_pullback_opened_on_bar_low = false; + double g_sync_wave_high = 0.0; // 已挂单的波段高价(用于检测换波段) double g_sync_wave_low = 0.0; @@ -92,8 +118,17 @@ 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); +void CancelAllEaPendingOrders(); +void CancelEaPendingOrderType(const ENUM_ORDER_TYPE order_type); +void CancelEaPendingAtHigh(); +void CancelEaPendingAtLow(); ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type); +ENUM_ORDER_TYPE PendingTypeOnHighBreakout(); +ENUM_ORDER_TYPE PendingTypeOnLowBreakout(); +long PosTypeOnHighBreakout(); +long PosTypeOnLowBreakout(); +void SyncPendingAtExtreme(const bool at_high, const double trigger_price, + const double wave_high, const double wave_low, const double lots); 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); @@ -105,6 +140,20 @@ double NormalizeVolumeLots(double lots); void ManagePositions(); void CheckTrailingStop(ulong ticket); void CheckAndCloseManualOrders(); +void CheckPullbackReverseSignals(); +void TryArmPullbackWatch(); +void ProcessPullbackReverseOnNewBar(); +ENUM_ORDER_TYPE OrderTypeOnHighPullbackReverse(); +ENUM_ORDER_TYPE OrderTypeOnLowPullbackReverse(); +bool IsPullbackBarOpenAllowed(const bool for_high_extreme); +void MarkPullbackBarOpened(const bool for_high_extreme); +void SyncPullbackBarLock(const bool for_high_extreme); +bool IsPullbackOpenBlocked(); +bool OpenPullbackReversePosition(ENUM_ORDER_TYPE order_type, const bool from_high_extreme, + const double extreme_price, const double wave_high, + const double wave_low); +bool CalcMarketSlTp(const ENUM_ORDER_TYPE order_type, const double entry_price, double &sl, + double &tp); //+------------------------------------------------------------------+ //| Expert initialization function | @@ -130,6 +179,10 @@ int OnInit() latest_wave.update_time = 0; latest_wave.high_used = false; latest_wave.low_used = false; + latest_wave.high_pullback_reverse_used = false; + latest_wave.low_pullback_reverse_used = false; + g_high_pullback_watch.active = false; + g_low_pullback_watch.active = false; Print("========================================"); Print("突破交易策略EA初始化成功"); @@ -154,7 +207,15 @@ int OnInit() Print("手数模式: 结余风险比例 ", InpRiskPercent, "% (按止损距离反推手数)"); else Print("手数模式: 固定手数 ", InpFixedLots); - Print("开仓方式: 突破挂单 (高点BUY STOP / 低点SELL STOP)"); + if(InpBreakoutDirection == BREAKOUT_DIR_FOLLOW) + Print("开仓方向: 顺势(破高多/破低空) 挂单: 高BUY STOP / 低SELL STOP"); + else + Print("开仓方向: 反向(破高空/破低多) 挂单: 高SELL LIMIT / 低BUY LIMIT"); + if(InpEnablePullbackReverse) + Print("回落反向: 开 破极值后监测", InpPullbackReverseWatchBars, + "根K收盘回到极值内开反向单(与突破挂单独立)"); + else + Print("回落反向: 关"); Print("禁止手工单:", (InpCloseManualOrders ? "启用 (自动平掉手工单)" : "禁用")); Print("========================================"); @@ -171,7 +232,7 @@ void OnDeinit(const int reason) // 删除所有标记 ObjectsDeleteAll(0, "ValidWave_"); - CancelEaPendingOrders(true, true); + CancelAllEaPendingOrders(); Print("突破交易策略EA已卸载"); } @@ -188,12 +249,22 @@ void OnTradeTransaction(const MqlTradeTransaction& trans, 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; + const string deal_comment = HistoryDealGetString(trans.deal, DEAL_COMMENT); + if(StringFind(deal_comment, "-PB") < 0) { + const ENUM_DEAL_TYPE deal_type = + (ENUM_DEAL_TYPE)HistoryDealGetInteger(trans.deal, DEAL_TYPE); + if(deal_type == DEAL_TYPE_BUY) { + if(PosTypeOnHighBreakout() == POSITION_TYPE_BUY) + latest_wave.high_used = true; + else if(PosTypeOnLowBreakout() == POSITION_TYPE_BUY) + latest_wave.low_used = true; + } else if(deal_type == DEAL_TYPE_SELL) { + if(PosTypeOnHighBreakout() == POSITION_TYPE_SELL) + latest_wave.high_used = true; + else if(PosTypeOnLowBreakout() == POSITION_TYPE_SELL) + latest_wave.low_used = true; + } + } } } } @@ -276,10 +347,13 @@ void OnTick() // 2. 更新最新有效波段 UpdateLatestValidWave(); - // 3. 同步突破挂单(BUY STOP@高 / SELL STOP@低) + // 3. 突破回落反向(破极值后K线收盘回到极值内,与突破挂单独立) + CheckPullbackReverseSignals(); + + // 4. 同步突破挂单(顺势/反向由 InpBreakoutDirection 决定) SyncBreakoutPendingOrders(); - // 4. 管理已有持仓 + // 5. 管理已有持仓 ManagePositions(); } @@ -552,12 +626,24 @@ void UpdateLatestValidWave() high != latest_wave.high_price || low != latest_wave.low_price) { + const double prev_high = latest_wave.high_price; + const double prev_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(high != prev_high) { + latest_wave.high_used = false; + latest_wave.high_pullback_reverse_used = false; + g_high_pullback_watch.active = false; + } + if(low != prev_low) { + latest_wave.low_used = false; + latest_wave.low_pullback_reverse_used = false; + g_low_pullback_watch.active = false; + } // 绘制最新有效波段 if(InpShowMarkers) { @@ -697,23 +783,57 @@ void FilterBreakouts(const int &breakout_bars[], const int &breakout_types[], //+------------------------------------------------------------------+ //| 突破挂单管理 | //+------------------------------------------------------------------+ -void CancelEaPendingOrders(const bool cancel_buy_stop, const bool cancel_sell_stop) +long PosTypeOnHighBreakout() { - 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(InpBreakoutDirection == BREAKOUT_DIR_REVERSE) + return POSITION_TYPE_SELL; + return POSITION_TYPE_BUY; +} - 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); - } +long PosTypeOnLowBreakout() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE) + return POSITION_TYPE_BUY; + return POSITION_TYPE_SELL; +} + +ENUM_ORDER_TYPE PendingTypeOnHighBreakout() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE) + return ORDER_TYPE_SELL_LIMIT; + return ORDER_TYPE_BUY_STOP; +} + +ENUM_ORDER_TYPE PendingTypeOnLowBreakout() +{ + if(InpBreakoutDirection == BREAKOUT_DIR_REVERSE) + return ORDER_TYPE_BUY_LIMIT; + return ORDER_TYPE_SELL_STOP; +} + +void CancelEaPendingOrderType(const ENUM_ORDER_TYPE order_type) +{ + const ulong ticket = FindEaPendingOrder(order_type); + if(ticket > 0) + trade.OrderDelete(ticket); +} + +void CancelEaPendingAtHigh() +{ + CancelEaPendingOrderType(ORDER_TYPE_BUY_STOP); + CancelEaPendingOrderType(ORDER_TYPE_SELL_LIMIT); +} + +void CancelEaPendingAtLow() +{ + CancelEaPendingOrderType(ORDER_TYPE_SELL_STOP); + CancelEaPendingOrderType(ORDER_TYPE_BUY_LIMIT); +} + +void CancelAllEaPendingOrders() +{ + CancelEaPendingAtHigh(); + CancelEaPendingAtLow(); } ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type) @@ -742,7 +862,7 @@ bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_pr sl = 0.0; tp = 0.0; - if(pending_type == ORDER_TYPE_BUY_STOP) { + if(pending_type == ORDER_TYPE_BUY_STOP || pending_type == ORDER_TYPE_BUY_LIMIT) { sl = NormalizeDouble(trigger_price - stop_loss_amount, _Digits); tp = NormalizeDouble(trigger_price + take_profit_amount, _Digits); if(stops_level > 0) { @@ -751,7 +871,7 @@ bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_pr if(tp - trigger_price < min_stop_distance) tp = NormalizeDouble(trigger_price + min_stop_distance, _Digits); } - } else if(pending_type == ORDER_TYPE_SELL_STOP) { + } else if(pending_type == ORDER_TYPE_SELL_STOP || pending_type == ORDER_TYPE_SELL_LIMIT) { sl = NormalizeDouble(trigger_price + stop_loss_amount, _Digits); tp = NormalizeDouble(trigger_price - take_profit_amount, _Digits); if(stops_level > 0) { @@ -785,6 +905,10 @@ bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double 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); + else if(pending_type == ORDER_TYPE_BUY_LIMIT) + result = trade.BuyLimit(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + else if(pending_type == ORDER_TYPE_SELL_LIMIT) + result = trade.SellLimit(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); if(!result) { Print("挂单失败 ", EnumToString(pending_type), " 触发价:", trigger_price, @@ -799,10 +923,54 @@ bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double return true; } +void SyncPendingAtExtreme(const bool at_high, const double trigger_price, + const double wave_high, const double wave_low, + const double lots) +{ + const ENUM_ORDER_TYPE pending_type = at_high ? + PendingTypeOnHighBreakout() : PendingTypeOnLowBreakout(); + const ENUM_ORDER_TYPE stale_type = at_high ? + (pending_type == ORDER_TYPE_BUY_STOP ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_BUY_STOP) : + (pending_type == ORDER_TYPE_SELL_STOP ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_STOP); + CancelEaPendingOrderType(stale_type); + + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const bool price_crossed = at_high ? + (ask >= trigger_price - _Point * 0.5) : + (bid <= trigger_price + _Point * 0.5); + + if(price_crossed) { + if(at_high) + CancelEaPendingAtHigh(); + else + CancelEaPendingAtLow(); + if(InpShowDebugInfo) + Print("价格已越过波段", (at_high ? "高点" : "低点"), + ",暂不挂", EnumToString(pending_type), + " ", (at_high ? "Ask:" : "Bid:"), + (at_high ? ask : bid), " 极值:", trigger_price); + return; + } + + ulong ticket = FindEaPendingOrder(pending_type); + 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 - trigger_price) > _Point * 0.5 || + MathAbs(order_lots - lots) > 1e-8) { + trade.OrderDelete(ticket); + ticket = 0; + } + } + if(ticket == 0) + PlaceBreakoutPendingOrder(pending_type, trigger_price, wave_high, wave_low); +} + void SyncBreakoutPendingOrders() { if(!latest_wave.exists) { - CancelEaPendingOrders(true, true); + CancelAllEaPendingOrders(); g_sync_wave_high = 0.0; g_sync_wave_low = 0.0; return; @@ -811,7 +979,7 @@ void SyncBreakoutPendingOrders() 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); + CancelAllEaPendingOrders(); return; } if(freeze_bar_index > 0) { @@ -824,7 +992,7 @@ void SyncBreakoutPendingOrders() 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); + CancelAllEaPendingOrders(); g_sync_wave_high = latest_wave.high_price; g_sync_wave_low = latest_wave.low_price; } @@ -849,67 +1017,296 @@ void SyncBreakoutPendingOrders() } if(total_positions >= InpMaxPositions) { - CancelEaPendingOrders(true, true); + CancelAllEaPendingOrders(); 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); - } + const long high_pos_type = PosTypeOnHighBreakout(); + const long low_pos_type = PosTypeOnLowBreakout(); + const int high_dir_positions = (high_pos_type == POSITION_TYPE_BUY) ? + buy_positions : sell_positions; + const int low_dir_positions = (low_pos_type == POSITION_TYPE_BUY) ? + buy_positions : sell_positions; + + if(!latest_wave.high_used && + (!InpOnePositionPerDirection || high_dir_positions < 1)) { + SyncPendingAtExtreme(true, wave_high, wave_high, wave_low, lots); } else { - CancelEaPendingOrders(true, false); + CancelEaPendingAtHigh(); } - // 低点 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); + if(!latest_wave.low_used && + (!InpOnePositionPerDirection || low_dir_positions < 1)) { + SyncPendingAtExtreme(false, wave_low, wave_high, wave_low, lots); + } else { + CancelEaPendingAtLow(); + } +} + +//+------------------------------------------------------------------+ +//| 突破回落反向(破极值后收盘回到极值内开反向单,与突破挂单独立) | +//+------------------------------------------------------------------+ +ENUM_ORDER_TYPE OrderTypeOnHighPullbackReverse() +{ + return ORDER_TYPE_SELL; +} + +ENUM_ORDER_TYPE OrderTypeOnLowPullbackReverse() +{ + return ORDER_TYPE_BUY; +} + +void SyncPullbackBarLock(const bool for_high_extreme) +{ + const datetime bar_time = iTime(_Symbol, InpTimeframe, 0); + if(bar_time == 0) + return; + if(for_high_extreme) { + if(bar_time != g_pullback_bar_time_high) { + g_pullback_bar_time_high = bar_time; + g_pullback_opened_on_bar_high = false; } } else { - CancelEaPendingOrders(false, true); + if(bar_time != g_pullback_bar_time_low) { + g_pullback_bar_time_low = bar_time; + g_pullback_opened_on_bar_low = false; + } } } +bool IsPullbackBarOpenAllowed(const bool for_high_extreme) +{ + SyncPullbackBarLock(for_high_extreme); + if(for_high_extreme) + return !g_pullback_opened_on_bar_high; + return !g_pullback_opened_on_bar_low; +} + +void MarkPullbackBarOpened(const bool for_high_extreme) +{ + SyncPullbackBarLock(for_high_extreme); + if(for_high_extreme) + g_pullback_opened_on_bar_high = true; + else + g_pullback_opened_on_bar_low = true; +} + +bool IsPullbackOpenBlocked() +{ + if(InpConsecutiveLosses > 0 && InpFreezeBarCount > 0 && freeze_bar_index > 0) { + if(Bars(_Symbol, InpTimeframe) < freeze_bar_index) + return true; + } + + 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) + return true; + + return false; +} + +bool IsPullbackDirectionBlocked(const ENUM_ORDER_TYPE order_type) +{ + if(!InpOnePositionPerDirection) + return false; + + const long pos_type = (order_type == ORDER_TYPE_BUY) ? + POSITION_TYPE_BUY : POSITION_TYPE_SELL; + + 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; + if(PositionGetInteger(POSITION_TYPE) == pos_type) + return true; + } + return false; +} + +bool CalcMarketSlTp(const ENUM_ORDER_TYPE order_type, const double entry_price, double &sl, + double &tp) +{ + const ENUM_ORDER_TYPE pending_equiv = (order_type == ORDER_TYPE_BUY) ? + ORDER_TYPE_BUY_STOP : ORDER_TYPE_SELL_STOP; + return CalcPendingSlTp(pending_equiv, entry_price, sl, tp); +} + +bool OpenPullbackReversePosition(ENUM_ORDER_TYPE order_type, const bool from_high_extreme, + const double extreme_price, const double wave_high, + const double wave_low) +{ + if(!IsPullbackBarOpenAllowed(from_high_extreme)) + return false; + + if(IsPullbackOpenBlocked()) { + if(InpShowDebugInfo) + Print("【回落反向】开仓跳过 - 冷冻中或已达最大持仓笔数"); + return false; + } + + if(IsPullbackDirectionBlocked(order_type)) { + if(InpShowDebugInfo) + Print("【回落反向】开仓跳过 - 同向已有持仓"); + return false; + } + + const double lots = CalculateLotSize(wave_high, wave_low); + if(lots <= 0.0) + return false; + + const double entry_price = (order_type == ORDER_TYPE_BUY) ? + SymbolInfoDouble(_Symbol, SYMBOL_ASK) : + SymbolInfoDouble(_Symbol, SYMBOL_BID); + + double sl = 0.0, tp = 0.0; + if(!CalcMarketSlTp(order_type, entry_price, sl, tp)) + return false; + + const int wave_range_points = (int)MathRound(MathAbs(wave_high - wave_low) / _Point); + const string comment = StringFormat("WR%d-PB", wave_range_points); + + bool result = false; + if(order_type == ORDER_TYPE_BUY) + result = trade.Buy(lots, _Symbol, entry_price, sl, tp, comment); + else + result = trade.Sell(lots, _Symbol, entry_price, sl, tp, comment); + + if(!result) { + Print("【回落反向】开仓失败: ", trade.ResultRetcode(), " - ", + trade.ResultRetcodeDescription()); + return false; + } + + MarkPullbackBarOpened(from_high_extreme); + Print("【回落反向】开仓成功 ", (order_type == ORDER_TYPE_BUY ? "多" : "空"), + " 手数:", lots, " 极值:", extreme_price, " SL:", sl, " TP:", tp); + return true; +} + +void TryArmPullbackWatch() +{ + if(!InpEnablePullbackReverse || !latest_wave.exists) + return; + + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + if(ask > latest_wave.high_price && !latest_wave.high_pullback_reverse_used) { + if(!g_high_pullback_watch.active || + g_high_pullback_watch.extreme_price != latest_wave.high_price) { + g_high_pullback_watch.active = true; + g_high_pullback_watch.extreme_price = latest_wave.high_price; + g_high_pullback_watch.bars_checked = 0; + if(InpShowDebugInfo) + Print("【回落反向】开始监测破高 极值:", latest_wave.high_price, + " 共", InpPullbackReverseWatchBars, "根K"); + } + } + + if(bid < latest_wave.low_price && !latest_wave.low_pullback_reverse_used) { + if(!g_low_pullback_watch.active || + g_low_pullback_watch.extreme_price != latest_wave.low_price) { + g_low_pullback_watch.active = true; + g_low_pullback_watch.extreme_price = latest_wave.low_price; + g_low_pullback_watch.bars_checked = 0; + if(InpShowDebugInfo) + Print("【回落反向】开始监测破低 极值:", latest_wave.low_price, + " 共", InpPullbackReverseWatchBars, "根K"); + } + } +} + +void ProcessPullbackReverseOnNewBar() +{ + if(!InpEnablePullbackReverse || !latest_wave.exists) + return; + + const int max_bars = MathMax(1, InpPullbackReverseWatchBars); + const double wave_high = latest_wave.high_price; + const double wave_low = latest_wave.low_price; + + if(g_high_pullback_watch.active && !latest_wave.high_pullback_reverse_used) { + const double close1 = iClose(_Symbol, InpTimeframe, 1); + if(close1 > 0.0 && close1 <= g_high_pullback_watch.extreme_price) { + if(OpenPullbackReversePosition(OrderTypeOnHighPullbackReverse(), true, + g_high_pullback_watch.extreme_price, + wave_high, wave_low)) { + latest_wave.high_pullback_reverse_used = true; + } + g_high_pullback_watch.active = false; + } else { + g_high_pullback_watch.bars_checked++; + if(g_high_pullback_watch.bars_checked >= max_bars) { + if(InpShowDebugInfo) + Print("【回落反向】破高监测结束 未回落极值内"); + g_high_pullback_watch.active = false; + } + } + } + + if(g_low_pullback_watch.active && !latest_wave.low_pullback_reverse_used) { + const double close1 = iClose(_Symbol, InpTimeframe, 1); + if(close1 > 0.0 && close1 >= g_low_pullback_watch.extreme_price) { + if(OpenPullbackReversePosition(OrderTypeOnLowPullbackReverse(), false, + g_low_pullback_watch.extreme_price, + wave_high, wave_low)) { + latest_wave.low_pullback_reverse_used = true; + } + g_low_pullback_watch.active = false; + } else { + g_low_pullback_watch.bars_checked++; + if(g_low_pullback_watch.bars_checked >= max_bars) { + if(InpShowDebugInfo) + Print("【回落反向】破低监测结束 未回升极值内"); + g_low_pullback_watch.active = false; + } + } + } +} + +void CheckPullbackReverseSignals() +{ + if(!InpEnablePullbackReverse) + return; + + TryArmPullbackWatch(); + + static datetime s_last_bar_time = 0; + const datetime bar_time = iTime(_Symbol, InpTimeframe, 0); + if(bar_time == 0 || bar_time == s_last_bar_time) + return; + s_last_bar_time = bar_time; + + ProcessPullbackReverseOnNewBar(); +} + //+------------------------------------------------------------------+ //| 止损/止盈距离(点数) | //+------------------------------------------------------------------+ diff --git a/20260522-突破马丁/20260522_Breakout.mq5 b/20260522-突破马丁/20260522_Breakout.mq5 index 484067e..a6b93ee 100644 --- a/20260522-突破马丁/20260522_Breakout.mq5 +++ b/20260522-突破马丁/20260522_Breakout.mq5 @@ -3,7 +3,7 @@ //| 突破策略 - 单笔突破开仓 | //+------------------------------------------------------------------+ #property copyright "Breakout Strategy" -#property version "3.18" +#property version "3.20" #property strict #include @@ -30,7 +30,7 @@ input group "=== 波段识别参数 ===" input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // K线周期 input int InpMAPeriod = 14; // MA周期 input double InpMinWavePercent = 0.1; // 最小波段阈值百分比(%) -input double InpMaxWavePercent = 0.25; // 最大波段阈值百分比(%) +input double InpMaxWavePercent = 1.0; // 最大波段阈值百分比(%) input double InpPullbackTolerance = 0.05; // 反向突破容忍度(%) 0=不容忍 input int InpMinWaveBars = 3; // 有效波段最少K线数(含两端极值所在K) @@ -39,17 +39,17 @@ input ENUM_BREAKOUT_DIRECTION InpBreakoutDirection = BREAKOUT_DIR_RANDOM; // 开 input double InpDailyMADevThreshold = 1.0; // 日MA偏离率阈值(%),|偏离|超过才触发反向,0=关 input int InpBatchTakeProfitPoints = 300; // 按批统盈点数(0=关):均价±点数平该批 input double InpStopLossBalancePct = 30.0; // 止损:持仓浮亏达结余比例(%)全部清仓,0=关 -input double InpLargeLotMinVolume = 1.0; // 大单独立止盈手数阈值(>=,0=关) -input int InpLargeLotTakeProfitPoints = 500; // 大单最大止盈点数(0=关,不参与统盈) -input bool InpEnablePullbackReverse = true; // 启用突破回落反向 +input double InpLargeLotMinVolume = 0.0; // 大单独立止盈手数阈值(>=,0=关) +input int InpLargeLotTakeProfitPoints = 0; // 大单最大止盈点数(0=关,不参与统盈) +input bool InpEnablePullbackReverse = false; // 启用突破回落反向 input int InpPullbackReverseWatchBars = 3; // 回落监测K线数(突破K+后2根=3) input group "=== 仓位管理参数 ===" input string InpTierLotsList = - "0.05,0.05,0.05,0.8,0.8,1.1,1,2,2,1,1,1,2,2,1,1,1,5,5"; // 各档手数(逗号分隔,档数=最多开仓笔数) + "0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,5,4,3,2,1,5"; // 各档手数(逗号分隔,档数=最多开仓笔数) input double InpMaxLots = 99.0; // 同向最大合计手数(0=不限制) input double InpMinSameDirAddSpacingPercent = 0.0; // 同向最小开仓间距(%) 0=不限制 -input ENUM_DUAL_FULL_ACTION InpDualFullAction = DUAL_FULL_CLEAR_ALL; // 双向满档后处理 +input ENUM_DUAL_FULL_ACTION InpDualFullAction = DUAL_FULL_NEW_ROUND; // 双向满档后处理 input group "=== 调试选项 ===" input bool InpShowDebugInfo = false; // 显示调试信息 @@ -131,6 +131,8 @@ int CountAllBreakoutPositions(); double TotalLotsInDirection(const long pos_type); bool IsDirectionFullActive(const long pos_type); void ResetBreakoutWaveEntryFlags(); +void ResetTradingStateAfterManualClose(); +bool IsManualDealCloseReason(const long reason); void CheckDualSideFullCapacity(); double NormalizeVolume(double lots); bool InitTierLotsFromInput(); @@ -1173,6 +1175,24 @@ void ResetBreakoutWaveEntryFlags() g_low_pullback_watch.active = false; } +bool IsManualDealCloseReason(const long reason) +{ + return (reason == DEAL_REASON_CLIENT || + reason == DEAL_REASON_MOBILE || + reason == DEAL_REASON_WEB); +} + +void ResetTradingStateAfterManualClose() +{ + g_trade_round = 1; + g_dual_full_handled = false; + ResetBreakoutWaveEntryFlags(); + g_breakout_opened_on_bar_high = false; + g_breakout_opened_on_bar_low = false; + g_pullback_opened_on_bar_high = false; + g_pullback_opened_on_bar_low = false; +} + int CountAllBreakoutPositions() { int count = 0; @@ -1974,7 +1994,7 @@ void CheckAndCloseManualOrders() } //+------------------------------------------------------------------+ -//| 成交事件(大单被平台TP平仓时重置满档状态) | +//| 成交事件(手动平仓重置状态 / 大单TP重置满档) | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, @@ -1990,18 +2010,27 @@ void OnTradeTransaction(const MqlTradeTransaction& trans, return; if(HistoryDealGetInteger(trans.deal, DEAL_ENTRY) != DEAL_ENTRY_OUT) return; - if(HistoryDealGetInteger(trans.deal, DEAL_REASON) != DEAL_REASON_TP) - return; const string comment = HistoryDealGetString(trans.deal, DEAL_COMMENT); if(!IsEaBreakoutComment(comment)) return; + const long reason = HistoryDealGetInteger(trans.deal, DEAL_REASON); const double vol = HistoryDealGetDouble(trans.deal, DEAL_VOLUME); + const int round_id = ParseRoundFromComment(comment); + + if(IsManualDealCloseReason(reason)) { + ResetTradingStateAfterManualClose(); + Print("【手动平仓】已重置轮次/满档/极值锁 手数:", vol, " 原轮次:", round_id, + " 备注:", comment, " (马丁档按剩余第1轮持仓重计)"); + return; + } + + if(reason != DEAL_REASON_TP) + return; if(!IsLargeLotVolume(vol)) return; - const int round_id = ParseRoundFromComment(comment); if(round_id == g_trade_round) g_dual_full_handled = false; diff --git a/20260522-突破马丁/README.md b/20260522-突破马丁/README.md index 9ead0db..a692942 100644 --- a/20260522-突破马丁/README.md +++ b/20260522-突破马丁/README.md @@ -1,57 +1,133 @@ # 突破策略(极值点突破开仓) -MQL5 EA:`20260522_Breakout.mq5`(v3.18)。 + + +MQL5 EA:`20260522_Breakout.mq5`(v3.20)。 + + ## 参数默认值(与输入界面一致) + + | 分组 | 参数 | 默认 | + |------|------|------| + | 波段 | K线周期 / MA周期 | M1 / 14 | -| 波段 | 最小/最大波段% | 0.1 / **0.25** | + +| 波段 | 最小/最大波段% | 0.1 / **1.0** | + | 波段 | 有效波段最少K线 | **3** | + | 波段 | 反向突破容忍度% | **0.05** | + | 交易 | 开仓方向 | **随机**(可选:顺势 / 反向 / 前日收线相对日MA / **日MA偏离率反向**) | + | 交易 | 日MA偏离率阈值% | **1.0**(\|偏离\|超过才触发,0=关) | + | 交易 | 按批统盈点数 / 止损% | **300** / **30** | -| 交易 | 大单止盈阈值 / 点数 | **1.0手** / **500点** | -| 交易 | 回落反向 / 监测K线数 | **开** / **3** | -| 仓位 | 各档手数(19档) | 见下 | -| 仓位 | 同向最大合计 / 最小间距% / 满档后 | 99 / **0** / **清仓** | + +| 交易 | 大单止盈阈值 / 点数 | **0(关)** / **0(关)** | + +| 交易 | 回落反向 / 监测K线数 | **关** / **3** | + +| 仓位 | 各档手数(14档) | 见下 | + +| 仓位 | 同向最大合计 / 最小间距% / 满档后 | 99 / **0** / **开新轮(不清仓)** | + | 调试 | 调试 / 极值标记 / Magic / 禁手工单 | false / true / 20260520 / true | -**各档手数:** `0.05,0.05,0.05,0.8,0.8,1.1,1,2,2,1,1,1,2,2,1,1,1,5,5`(19 档) + + +**各档手数:** `0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,5,4,3,2,1,5`(14 档) + + ## 前日收线相对日MA(开仓方向选项) + + 取**前一交易日**日 K **收盘价**与**日 K 上同周期 MA**(默认 MA14)比较: + + | 前日收线位置 | 允许开仓 | + |-------------|----------| + | **在日 MA 下方** | 破**低**极值 → 空;**高**极值出现回落反向 → 空 | + | **在日 MA 上方** | 破**高**极值 → 多;**低**极值出现回落反向 → 多 | + + - 偏空时不做「破高」突破单,等高极值**回落反向**或等破低;偏多时不做「破低」突破单,等低极值**回落反向**或等破高。 + - 前日收线恰在 MA 上(中性)时,该模式不开仓。 + + ## 日 MA 偏离率反向(开仓方向选项) + + **偏离率** = (当前价 − 日 MA) ÷ 日 MA × 100%(当前价取买卖中间价,日 MA 周期同 `InpMAPeriod`)。 + + | 偏离率 | 含义 | 允许开仓(反向/均值回归) | + |--------|------|--------------------------| + | **> +阈值%** | 价显著高于日 MA | 破**高** → 空;**高**极值回落反向 → 空 | + | **< −阈值%** | 价显著低于日 MA | 破**低** → 多;**低**极值回落反向 → 多 | + | \|偏离\| ≤ 阈值 | 未超阈值 | **不开仓** | + + 参数 `日MA偏离率阈值` 默认 **1.0%**,设为 **0** 关闭此逻辑。 + + ## 大单独立止盈 -- 开仓手数 **≥ 大单止盈阈值**(默认 1 手)的单子,按 **大单最大止盈点数** 单独止盈,**不参与按批统盈**的加权均价计算。 -- 大单止盈平仓后(平台 TP 或 EA 监测触发),**下一档马丁手数**与**统盈均价**均仅按**当前仍持仓**的单子重算,与已平大单无关。 -- 阈值或点数设为 **0** 时关闭此功能,所有单子统一走按批统盈。 + + +- 默认**关闭**(阈值 0 或点数 0)。开启后:开仓手数 **≥ 阈值** 的单子按 **点数** 单独止盈,**不参与按批统盈**。 + +- 大单止盈平仓后,马丁档与统盈均价按**剩余持仓**重计。 + + + +## 手动平仓 EA 单 + + + +在终端/手机/Web 上**手动平掉**本 EA 持仓(Magic 与备注 `[突破…]` 一致)时,会立即重置: + + + +- **交易轮次** → 第 1 轮(仅统计备注为第 1 轮的持仓计档;更高轮次旧仓仍保留,统盈仍按各自轮次批处理) + +- **双向满档**已处理标记 + +- **极值锁**:波段高/低突破与回落反向已用标记、回落监测、同 K 线限 1 笔 + + + +EA 程序平仓(统盈、止损、满档清仓、大单止盈等)**不会**触发上述重置。 + + ## 说明 + + - 突破开仓与回落反向共用档位手数;列表长度 = 同向最多开仓笔数。 + - 按批统盈、结余%止损、双向满档逻辑见 EA 输入注释。 + + diff --git a/20260526-突破-箱体/120_300.set b/20260526-突破-箱体/120_300.set new file mode 100644 index 0000000..6d66c45 Binary files /dev/null and b/20260526-突破-箱体/120_300.set differ diff --git a/20260526-突破-箱体/120_500.set b/20260526-突破-箱体/120_500.set new file mode 100644 index 0000000..bdae79a Binary files /dev/null and b/20260526-突破-箱体/120_500.set differ diff --git a/20260526-突破-箱体/1min_80_120.set b/20260526-突破-箱体/1min_80_120.set new file mode 100644 index 0000000..b36dfdd Binary files /dev/null and b/20260526-突破-箱体/1min_80_120.set differ diff --git a/20260526-突破-箱体/20260526_box_breakout.mq5 b/20260526-突破-箱体/20260526_box_breakout.mq5 new file mode 100644 index 0000000..31d9894 --- /dev/null +++ b/20260526-突破-箱体/20260526_box_breakout.mq5 @@ -0,0 +1,1721 @@ +//+------------------------------------------------------------------+ +//| 20260526_box_breakout.mq5 | +//| 聚合箱体突破策略 - 完整交易版本 | +//+------------------------------------------------------------------+ +#property copyright "Breakout Strategy" +#property version "1.21" +#property strict + +#include + +// 手数计算方式 +enum ENUM_LOT_SIZE_MODE { + LOT_SIZE_FIXED = 0, // 固定手数 + LOT_SIZE_RISK_PERCENT = 1 // 结余风险比例(RiskPercent) +}; + +// 破高/破低开仓方向 +enum ENUM_BREAKOUT_DIRECTION { + BREAKOUT_DIR_FOLLOW = 0, // 顺势(破高多/破低空) + BREAKOUT_DIR_REVERSE = 1 // 反向(破高空/破低多) +}; + +//+------------------------------------------------------------------+ +//| 输入参数 | +//+------------------------------------------------------------------+ +input group "=== 波段识别参数 ===" +input ENUM_TIMEFRAMES InpTimeframe = PERIOD_M1; // K线周期 +input int InpMAPeriod = 14; // MA周期 +input double InpMinWavePercent = 0.1; // 最小波段振幅百分比(%) +input double InpMaxWavePercent = 2.0; // 最大波段阈值百分比(%) +input double InpPullbackTolerance = 0.05; // 反向突破容忍度(%) 0=不容忍 +input int InpMinWaveBars = 3; // 有效波段最少K线数 + +input group "=== 大波段突破机制参数 ===" +input bool InpEnableLargeWaveTrade = true; // 启用大波段突破机制 +input ENUM_BREAKOUT_DIRECTION InpLargeWaveDirection = BREAKOUT_DIR_FOLLOW; // 开仓方向 +input double InpLargeWaveMinPercent = 0.25; // 最小波段振幅%(>该值) +input double InpLargeWaveMaxPercent = 2.0; // 最大波段振幅%(<=该值) +input bool InpLargeWaveRetryAfterStopLoss = false; // 边界止损后再挂一单 +input int InpLargeWaveStopLossPoints = 80; // 止损点数 +input int InpLargeWaveTakeProfitPoints = 120; // 止盈点数 +input bool InpLargeWaveUseTrailingStop = false; // 使用移动止损 + +input group "=== 聚合箱体机制参数 ===" +input bool InpEnableBoxTrade = true; // 启用聚合箱体突破机制 +input ENUM_BREAKOUT_DIRECTION InpBoxDirection = BREAKOUT_DIR_FOLLOW; // 开仓方向 +input double InpBoxWaveMinPercent = 0.1; // 成箱波段振幅下限%(>=) +input double InpBoxWaveMaxPercent = 0.25; // 成箱波段振幅上限%(<=) +input int InpBoxMinWaves = 1; // 成箱最少有效波段数 +input int InpBoxMaxWaves = 3; // 成箱最多有效波段数(0=不限制) +input bool InpBoxRetryAfterStopLoss = false; // 边界止损后再挂一单 +input int InpBoxStopLossPoints = 80; // 止损点数 +input int InpBoxTakeProfitPoints = 120; // 止盈点数 +input bool InpBoxUseTrailingStop = false; // 使用移动止损 + +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 = 10.0; // 单笔最大手数 + +input group "=== 其他参数 ===" +input bool InpCloseManualOrders = true; // 禁止手工单 +input int InpMagicNumber = 20260526; // EA魔术号 +input bool InpShowExtremeMarkers = false; // 显示极值点标记 +input bool InpBoxShowOnChart = false; // 显示箱体标记 +input bool InpShowLatestWaveMarker = false; // 显示最近有效波段标记 + +input group "=== 交易时段(北京时间) ===" +input bool InpEnableTradeSessionFilter = false; // 启用时段过滤 +input string InpTradeSessionsBeijing = "15:00-02:00"; // 可交易时段 多段用逗号分隔 + +//+------------------------------------------------------------------+ +//| 全局变量 | +//+------------------------------------------------------------------+ +#define MAX_TRADE_SESSIONS 8 + +struct TradeSessionSlot { + int start_minutes; + int end_minutes; + bool crosses_midnight; +}; + +TradeSessionSlot g_trade_sessions[MAX_TRADE_SESSIONS]; +int g_trade_session_count = 0; +bool g_last_tick_in_trade_session = 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; + bool high_sl_retry_done; + bool low_sl_retry_done; + double range_percent; +}; + +ValidWaveInfo latest_wave; + +double g_sync_wave_high = 0.0; +double g_sync_wave_low = 0.0; + +struct WaveSegment { + double high_price; + double low_price; + datetime time_start; + datetime time_end; + double range_percent; +}; + +// 聚合交易箱体 +struct AggregateBoxInfo { + bool exists; + double high_price; + double low_price; + datetime time_start; + datetime time_end; + int wave_count; + bool high_used; + bool low_used; + bool high_sl_retry_done; + bool low_sl_retry_done; + double range_percent; +}; + +AggregateBoxInfo active_box; +double g_sync_box_high = 0.0; +double g_sync_box_low = 0.0; +int g_box_draw_id = 0; + +// 极值点结构体定义 +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); +bool IsValidWavePair(const ExtremePoint &extremes[], const MqlRates &rates[], const int i); +void BuildValidWaveSegments(const ExtremePoint &extremes[], const MqlRates &rates[], + WaveSegment &segments[]); +void UpdateAggregateBox(const WaveSegment &segments[]); +void DrawAggregateBox(); +void DeleteAggregateBoxObjects(); +double RangePercent(const double high, const double low); +bool IsLargeWaveTradeEligible(); +bool IsBoxWaveSegmentPercent(const double pct); +void MarkLargeWaveHighUsed(); +void MarkLargeWaveLowUsed(); +void MarkBoxHighUsed(); +void MarkBoxLowUsed(); +void HandleStopLossRetryOnDeal(const ulong deal_ticket); +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 SyncLargeWaveBreakoutPendingOrders(); +void SyncBoxBreakoutPendingOrders(); +void CancelAllEaPendingOrders(); +void CancelLargeWavePendingOrders(); +void CancelBoxPendingOrders(); +void CancelEaPendingOrderType(const ENUM_ORDER_TYPE order_type, const string comment_prefix); +void CancelLargeWavePendingAtHigh(); +void CancelLargeWavePendingAtLow(); +void CancelBoxPendingAtHigh(); +void CancelBoxPendingAtLow(); +ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type, const string comment_prefix); +ENUM_ORDER_TYPE PendingTypeOnHighBreakout(const ENUM_BREAKOUT_DIRECTION dir); +ENUM_ORDER_TYPE PendingTypeOnLowBreakout(const ENUM_BREAKOUT_DIRECTION dir); +long PosTypeOnHighBreakout(const ENUM_BREAKOUT_DIRECTION dir); +long PosTypeOnLowBreakout(const ENUM_BREAKOUT_DIRECTION dir); +void SyncPendingAtExtreme(const bool at_high, const double trigger_price, + const double range_high, const double range_low, + const ENUM_BREAKOUT_DIRECTION dir, const int sl_points, + const int tp_points, const string comment_prefix); +bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const int sl_points, const int tp_points, double &sl, double &tp); +bool PlaceBreakoutPendingOrder(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const int sl_points, const int tp_points, + const string comment); +double StopLossOffsetPoints(const int sl_points); +double TakeProfitOffsetPoints(const int tp_points); +int StopLossPointsFromComment(const string &comment); +bool UseTrailingStopFromComment(const string &comment); +double CalculateLotSize(const int sl_points); +double NormalizeVolumeLots(double lots); +void ManagePositions(); +void CheckTrailingStop(ulong ticket); +void CheckAndCloseManualOrders(); +bool ParseHHMMToMinutes(const string hhmm, int &minutes); +bool ParseTradeSessionsFromString(const string spec); +int BeijingMinutesOfDay(); +bool IsWithinTradeSession(); +void EnforceTradeSessionOnTick(); +void LogDealFees(const ulong deal_ticket); + +//+------------------------------------------------------------------+ +//| 交易时段(北京时间 UTC+8) | +//+------------------------------------------------------------------+ +bool ParseHHMMToMinutes(const string hhmm, int &minutes) +{ + string s = hhmm; + StringTrimLeft(s); + StringTrimRight(s); + const int colon = StringFind(s, ":"); + if(colon <= 0) + return false; + + const int hour = (int)StringToInteger(StringSubstr(s, 0, colon)); + const int minute = (int)StringToInteger(StringSubstr(s, colon + 1)); + if(hour < 0 || hour > 23 || minute < 0 || minute > 59) + return false; + + minutes = hour * 60 + minute; + return true; +} + +bool ParseTradeSessionsFromString(const string spec) +{ + g_trade_session_count = 0; + + string s = spec; + StringTrimLeft(s); + StringTrimRight(s); + if(s == "") + return false; + + string parts[]; + const int part_count = StringSplit(s, ',', parts); + for(int i = 0; i < part_count && g_trade_session_count < MAX_TRADE_SESSIONS; i++) { + string slot = parts[i]; + StringTrimLeft(slot); + StringTrimRight(slot); + if(slot == "") + continue; + + const int dash = StringFind(slot, "-"); + if(dash <= 0) + continue; + + string start_str = StringSubstr(slot, 0, dash); + string end_str = StringSubstr(slot, dash + 1); + StringTrimLeft(start_str); + StringTrimRight(start_str); + StringTrimLeft(end_str); + StringTrimRight(end_str); + + int start_min = 0, end_min = 0; + if(!ParseHHMMToMinutes(start_str, start_min) || !ParseHHMMToMinutes(end_str, end_min)) + continue; + + const int idx = g_trade_session_count; + g_trade_sessions[idx].start_minutes = start_min; + g_trade_sessions[idx].end_minutes = end_min; + g_trade_sessions[idx].crosses_midnight = (start_min > end_min); + g_trade_session_count++; + } + + return (g_trade_session_count > 0); +} + +int BeijingMinutesOfDay() +{ + MqlDateTime dt; + TimeToStruct(TimeGMT() + 8 * 3600, dt); + return dt.hour * 60 + dt.min; +} + +bool IsWithinTradeSession() +{ + if(!InpEnableTradeSessionFilter || g_trade_session_count <= 0) + return true; + + const int now = BeijingMinutesOfDay(); + for(int i = 0; i < g_trade_session_count; i++) { + if(g_trade_sessions[i].crosses_midnight) { + if(now >= g_trade_sessions[i].start_minutes || + now < g_trade_sessions[i].end_minutes) + return true; + } else if(now >= g_trade_sessions[i].start_minutes && + now < g_trade_sessions[i].end_minutes) { + return true; + } + } + return false; +} + +void EnforceTradeSessionOnTick() +{ + const bool in_session = IsWithinTradeSession(); + + if(!in_session) + CancelAllEaPendingOrders(); + + if(in_session != g_last_tick_in_trade_session) { + MqlDateTime dt; + TimeToStruct(TimeGMT() + 8 * 3600, dt); + Print("【交易时段】北京时间 ", + StringFormat("%02d:%02d", dt.hour, dt.min), + in_session ? " 进入可交易时段" : " 离开可交易时段(已撤EA挂单)"); + g_last_tick_in_trade_session = in_session; + } +} + +//+------------------------------------------------------------------+ +//| 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; + latest_wave.high_sl_retry_done = false; + latest_wave.low_sl_retry_done = false; + + active_box.exists = false; + active_box.high_used = false; + active_box.low_used = false; + active_box.high_sl_retry_done = false; + active_box.low_sl_retry_done = false; + g_sync_box_high = 0.0; + g_sync_box_low = 0.0; + g_box_draw_id = 0; + + Print("========================================"); + Print("20260526_box_breakout 初始化成功"); + Print("品种:", _Symbol, " Magic:", InpMagicNumber); + Print("结构有效波段: ", InpMinWavePercent, "% - ", InpMaxWavePercent, "%"); + Print("大波段突破:", (InpEnableLargeWaveTrade ? "开" : "关"), + " (", InpLargeWaveMinPercent, "%, ", InpLargeWaveMaxPercent, "%]"); + Print("聚合箱体:", (InpEnableBoxTrade ? "开" : "关"), + " 成箱波段 [", InpBoxWaveMinPercent, "%, ", InpBoxWaveMaxPercent, "%] 段数", + InpBoxMinWaves, "-", (InpBoxMaxWaves > 0 ? IntegerToString(InpBoxMaxWaves) : "不限")); + if(InpBoxMaxWaves > 0 && InpBoxMaxWaves < InpBoxMinWaves) + Print("【参数警告】成箱最多段数 < 最少段数, 将无法成箱"); + if(InpEnableTradeSessionFilter) { + if(ParseTradeSessionsFromString(InpTradeSessionsBeijing)) + Print("交易时段过滤: 开 北京时间 [", InpTradeSessionsBeijing, "]"); + else + Print("【参数警告】可交易时段解析失败, 时段过滤不生效"); + } else { + Print("交易时段过滤: 关"); + } + g_last_tick_in_trade_session = IsWithinTradeSession(); + Print("========================================"); + + return(INIT_SUCCEEDED); +} + +//+------------------------------------------------------------------+ +//| Expert deinitialization function | +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) +{ + if(ma_handle != INVALID_HANDLE) + IndicatorRelease(ma_handle); + + // 删除所有标记 + ObjectsDeleteAll(0, "ValidWave_"); + DeleteAggregateBoxObjects(); + CancelAllEaPendingOrders(); + + Print("突破交易策略EA已卸载"); +} + +//+------------------------------------------------------------------+ +//| 日志:成交手续费/库存费/净利(来自测试器或券商 deal 记录) | +//+------------------------------------------------------------------+ +void LogDealFees(const ulong deal_ticket) +{ + if(!HistoryDealSelect(deal_ticket)) + return; + + const string comment = HistoryDealGetString(deal_ticket, DEAL_COMMENT); + if(StringFind(comment, "LW") != 0 && StringFind(comment, "BX") != 0) + return; + + const ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); + string entry_label = "成交"; + if(entry == DEAL_ENTRY_IN) + entry_label = "开仓"; + else if(entry == DEAL_ENTRY_OUT) + entry_label = "平仓"; + + const double profit = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); + const double commission = HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); + const double swap = HistoryDealGetDouble(deal_ticket, DEAL_SWAP); + const double net = profit + commission + swap; + const double volume = HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); + const double price = HistoryDealGetDouble(deal_ticket, DEAL_PRICE); + const ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(deal_ticket, DEAL_REASON); + + Print("【成交费用】", entry_label, + " deal=", deal_ticket, + " ", comment, + " 手=", DoubleToString(volume, 2), + " 价=", DoubleToString(price, _Digits), + " 原因=", EnumToString(reason), + " 毛利=", DoubleToString(profit, 2), + " 手续费=", DoubleToString(commission, 2), + " 库存费=", DoubleToString(swap, 2), + " 净利=", DoubleToString(net, 2)); +} + +//+------------------------------------------------------------------+ +//| 交易事务处理函数 - 用于检测亏损单 | +//+------------------------------------------------------------------+ +void OnTradeTransaction(const MqlTradeTransaction& trans, + const MqlTradeRequest& request, + const MqlTradeResult& result) +{ + if(trans.type != TRADE_TRANSACTION_DEAL_ADD) + return; + if(!HistoryDealSelect(trans.deal)) + return; + if(HistoryDealGetString(trans.deal, DEAL_SYMBOL) != _Symbol) + return; + if(HistoryDealGetInteger(trans.deal, DEAL_MAGIC) != InpMagicNumber) + return; + + LogDealFees(trans.deal); + + const ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY); + if(entry == DEAL_ENTRY_IN) { + const string deal_comment = HistoryDealGetString(trans.deal, DEAL_COMMENT); + const ENUM_DEAL_TYPE deal_type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(trans.deal, DEAL_TYPE); + if(StringFind(deal_comment, "LW") == 0) { + if(deal_type == DEAL_TYPE_BUY) { + if(PosTypeOnHighBreakout(InpLargeWaveDirection) == POSITION_TYPE_BUY) + MarkLargeWaveHighUsed(); + else if(PosTypeOnLowBreakout(InpLargeWaveDirection) == POSITION_TYPE_BUY) + MarkLargeWaveLowUsed(); + } else if(deal_type == DEAL_TYPE_SELL) { + if(PosTypeOnHighBreakout(InpLargeWaveDirection) == POSITION_TYPE_SELL) + MarkLargeWaveHighUsed(); + else if(PosTypeOnLowBreakout(InpLargeWaveDirection) == POSITION_TYPE_SELL) + MarkLargeWaveLowUsed(); + } + } else if(StringFind(deal_comment, "BX") == 0) { + if(deal_type == DEAL_TYPE_BUY) { + if(PosTypeOnHighBreakout(InpBoxDirection) == POSITION_TYPE_BUY) + MarkBoxHighUsed(); + else if(PosTypeOnLowBreakout(InpBoxDirection) == POSITION_TYPE_BUY) + MarkBoxLowUsed(); + } else if(deal_type == DEAL_TYPE_SELL) { + if(PosTypeOnHighBreakout(InpBoxDirection) == POSITION_TYPE_SELL) + MarkBoxHighUsed(); + else if(PosTypeOnLowBreakout(InpBoxDirection) == POSITION_TYPE_SELL) + MarkBoxLowUsed(); + } + } + } else if(entry == DEAL_ENTRY_OUT) { + HandleStopLossRetryOnDeal(trans.deal); + } +} + +//+------------------------------------------------------------------+ +//| Expert tick function | +//+------------------------------------------------------------------+ +void OnTick() +{ + // 1. 检查并关闭手工单(如果启用) + if(InpCloseManualOrders) + CheckAndCloseManualOrders(); + + // 2. 更新最新有效波段 + UpdateLatestValidWave(); + + // 3. 时段过滤(北京时间): 时段外撤挂单, 时段内才挂突破单 + EnforceTradeSessionOnTick(); + if(InpEnableTradeSessionFilter && !IsWithinTradeSession()) { + ManagePositions(); + return; + } + + // 4. 大波段/箱体突破挂单(两套逻辑互不影响) + SyncLargeWaveBreakoutPendingOrders(); + SyncBoxBreakoutPendingOrders(); + + // 5. 管理已有持仓 + 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++) { + if(IsValidWavePair(extremes, rates, i)) { + extremes[i-1].is_valid = true; + extremes[i].is_valid = true; + } + } + + // 绘制所有极值点标记 + if(InpShowExtremeMarkers) + DrawExtremeMarkers(extremes); + + // 查找最新的有效波段 + for(int i = ArraySize(extremes) - 1; i >= 1; i--) { + if(IsValidWavePair(extremes, rates, i)) { + const double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); + const double price_diff_points = price_diff / _Point; + const double base_price = extremes[i-1].price; + const double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; + const double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; + // 找到最新的有效波段 + 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) { + + const double prev_high = latest_wave.high_price; + const double prev_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.range_percent = RangePercent(high, low); + + if(high != prev_high) { + latest_wave.high_used = false; + latest_wave.high_sl_retry_done = false; + } + if(low != prev_low) { + latest_wave.low_used = false; + latest_wave.low_sl_retry_done = false; + } + + if(InpShowLatestWaveMarker) + DrawLatestValidWave(high, high_time, low, low_time); + } + break; + } + } + + if(latest_wave.exists) + latest_wave.range_percent = RangePercent(latest_wave.high_price, latest_wave.low_price); + else + latest_wave.range_percent = 0.0; + + WaveSegment segments[]; + BuildValidWaveSegments(extremes, rates, segments); + UpdateAggregateBox(segments); +} + +//+------------------------------------------------------------------+ +//| 有效波段判定(相邻两极值) | +//+------------------------------------------------------------------+ +bool IsValidWavePair(const ExtremePoint &extremes[], const MqlRates &rates[], const int i) +{ + if(i < 1 || i >= ArraySize(extremes)) + return false; + + const double price_diff = MathAbs(extremes[i].price - extremes[i-1].price); + const double price_diff_points = price_diff / _Point; + const double base_price = extremes[i-1].price; + const double min_threshold = (base_price * InpMinWavePercent / 100.0) / _Point; + const double max_threshold = (base_price * InpMaxWavePercent / 100.0) / _Point; + + return (price_diff_points >= min_threshold && price_diff_points <= max_threshold && + IsValidWaveByBarCount(rates, extremes[i - 1].time, extremes[i].time)); +} + +//+------------------------------------------------------------------+ +//| 收集全部有效波段(时间正序:旧→新) | +//+------------------------------------------------------------------+ +void BuildValidWaveSegments(const ExtremePoint &extremes[], const MqlRates &rates[], + WaveSegment &segments[]) +{ + ArrayResize(segments, 0); + + for(int i = 1; i < ArraySize(extremes); i++) { + if(!IsValidWavePair(extremes, rates, i)) + continue; + + const int size = ArraySize(segments); + ArrayResize(segments, size + 1); + segments[size].high_price = MathMax(extremes[i].price, extremes[i-1].price); + segments[size].low_price = MathMin(extremes[i].price, extremes[i-1].price); + segments[size].time_start = extremes[i-1].time; + segments[size].time_end = extremes[i].time; + segments[size].range_percent = RangePercent(segments[size].high_price, segments[size].low_price); + } +} + +//+------------------------------------------------------------------+ +//| 聚合箱体: 从新到旧纳入成箱振幅内的小波段 | +//+------------------------------------------------------------------+ +void UpdateAggregateBox(const WaveSegment &segments[]) +{ + if(!InpEnableBoxTrade && !InpBoxShowOnChart) { + if(active_box.exists) { + active_box.exists = false; + DeleteAggregateBoxObjects(); + } + return; + } + + const int total = ArraySize(segments); + if(total <= 0) { + if(active_box.exists) { + active_box.exists = false; + DeleteAggregateBoxObjects(); + } + return; + } + + const int newest_idx = total - 1; + if(!IsBoxWaveSegmentPercent(segments[newest_idx].range_percent)) { + if(active_box.exists) { + active_box.exists = false; + DeleteAggregateBoxObjects(); + } + return; + } + + double box_high = segments[newest_idx].high_price; + double box_low = segments[newest_idx].low_price; + datetime time_start = segments[newest_idx].time_start; + datetime time_end = segments[newest_idx].time_end; + int wave_count = 1; + + for(int i = newest_idx - 1; i >= 0; i--) { + if(!IsBoxWaveSegmentPercent(segments[i].range_percent)) + break; + if(InpBoxMaxWaves > 0 && wave_count >= InpBoxMaxWaves) + break; + + box_high = MathMax(box_high, segments[i].high_price); + box_low = MathMin(box_low, segments[i].low_price); + time_start = segments[i].time_start; + wave_count++; + } + + if(wave_count < InpBoxMinWaves) { + if(active_box.exists) { + active_box.exists = false; + DeleteAggregateBoxObjects(); + } + return; + } + + const bool prev_exists = active_box.exists; + const double prev_high = active_box.high_price; + const double prev_low = active_box.low_price; + const bool box_replaced = prev_exists && + (MathAbs(box_high - prev_high) > _Point * 0.5 || + MathAbs(box_low - prev_low) > _Point * 0.5); + + active_box.exists = true; + active_box.high_price = box_high; + active_box.low_price = box_low; + active_box.time_start = time_start; + active_box.time_end = time_end; + active_box.wave_count = wave_count; + active_box.range_percent = RangePercent(box_high, box_low); + + if(!prev_exists || box_replaced) { + active_box.high_used = false; + active_box.low_used = false; + active_box.high_sl_retry_done = false; + active_box.low_sl_retry_done = false; + g_box_draw_id++; + } + + if(InpBoxShowOnChart) + DrawAggregateBox(); +} + +//+------------------------------------------------------------------+ +//| 振幅 / 交易资格 / 已用标记 | +//+------------------------------------------------------------------+ +double RangePercent(const double high, const double low) +{ + const double mid = (high + low) * 0.5; + if(mid <= 0.0) + return 0.0; + return (high - low) / mid * 100.0; +} + +bool IsLargeWaveTradeEligible() +{ + if(!InpEnableLargeWaveTrade || !latest_wave.exists) + return false; + return (latest_wave.range_percent > InpLargeWaveMinPercent && + latest_wave.range_percent <= InpLargeWaveMaxPercent); +} + +bool IsBoxWaveSegmentPercent(const double pct) +{ + return (pct >= InpBoxWaveMinPercent && pct <= InpBoxWaveMaxPercent); +} + +void MarkLargeWaveHighUsed() +{ + latest_wave.high_used = true; +} + +void MarkLargeWaveLowUsed() +{ + latest_wave.low_used = true; +} + +void MarkBoxHighUsed() +{ + if(active_box.exists) + active_box.high_used = true; +} + +void MarkBoxLowUsed() +{ + if(active_box.exists) + active_box.low_used = true; +} + +//+------------------------------------------------------------------+ +//| 止损出局后按机制允许同边界再挂一次 | +//+------------------------------------------------------------------+ +void HandleStopLossRetryOnDeal(const ulong deal_ticket) +{ + if(!HistoryDealSelect(deal_ticket)) + return; + if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(deal_ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) + return; + + const ENUM_DEAL_REASON close_reason = + (ENUM_DEAL_REASON)HistoryDealGetInteger(deal_ticket, DEAL_REASON); + if(close_reason != DEAL_REASON_SL) + return; + + const ulong position_id = (ulong)HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); + if(position_id == 0 || !HistorySelectByPosition(position_id)) + return; + + long entry_pos_type = -1; + ENUM_BREAKOUT_DIRECTION entry_dir = InpLargeWaveDirection; + string entry_comment = ""; + for(int i = 0; i < HistoryDealsTotal(); i++) { + const ulong in_ticket = HistoryDealGetTicket(i); + if(in_ticket == 0) + continue; + if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(in_ticket, DEAL_ENTRY) != DEAL_ENTRY_IN) + continue; + const ENUM_DEAL_TYPE in_type = (ENUM_DEAL_TYPE)HistoryDealGetInteger(in_ticket, DEAL_TYPE); + if(in_type == DEAL_TYPE_BUY) + entry_pos_type = POSITION_TYPE_BUY; + else if(in_type == DEAL_TYPE_SELL) + entry_pos_type = POSITION_TYPE_SELL; + entry_comment = HistoryDealGetString(in_ticket, DEAL_COMMENT); + if(StringFind(entry_comment, "BX") == 0) + entry_dir = InpBoxDirection; + else + entry_dir = InpLargeWaveDirection; + break; + } + if(entry_pos_type < 0 || entry_comment == "") + return; + + const bool at_high = (entry_pos_type == PosTypeOnHighBreakout(entry_dir)); + const bool at_low = (entry_pos_type == PosTypeOnLowBreakout(entry_dir)); + if(!at_high && !at_low) + return; + + if(StringFind(entry_comment, "LW") == 0) { + if(!InpLargeWaveRetryAfterStopLoss) + return; + if(at_high) { + if(latest_wave.high_sl_retry_done) + return; + latest_wave.high_used = false; + latest_wave.high_sl_retry_done = true; + } else { + if(latest_wave.low_sl_retry_done) + return; + latest_wave.low_used = false; + latest_wave.low_sl_retry_done = true; + } + Print("【大波段】止损出局,允许边界再挂一次"); + return; + } + + if(StringFind(entry_comment, "BX") == 0) { + if(!InpBoxRetryAfterStopLoss || !active_box.exists) + return; + if(at_high) { + if(active_box.high_sl_retry_done) + return; + active_box.high_used = false; + active_box.high_sl_retry_done = true; + } else { + if(active_box.low_sl_retry_done) + return; + active_box.low_used = false; + active_box.low_sl_retry_done = true; + } + Print("【聚合箱体】止损出局,允许边界再挂一次"); + } +} + +void DeleteAggregateBoxObjects() +{ + ObjectsDeleteAll(0, "TradeBox_"); +} + +//+------------------------------------------------------------------+ +//| 图表绘制聚合箱体(矩形+上下沿) | +//+------------------------------------------------------------------+ +void DrawAggregateBox() +{ + if(!active_box.exists || !InpBoxShowOnChart) + return; + + const string prefix = "TradeBox_" + IntegerToString(g_box_draw_id) + "_"; + const datetime time_right = iTime(_Symbol, InpTimeframe, 0); + if(time_right == 0) + return; + + datetime time_left = active_box.time_start; + if(time_left <= 0 || time_left >= time_right) + time_left = active_box.time_end; + if(time_left <= 0 || time_left >= time_right) + time_left = time_right - (datetime)(PeriodSeconds(InpTimeframe) * 20); + + const double box_high = active_box.high_price; + const double box_low = active_box.low_price; + + string rect_name = prefix + "Rect"; + if(ObjectFind(0, rect_name) < 0) + ObjectCreate(0, rect_name, OBJ_RECTANGLE, 0, time_left, box_high, time_right, box_low); + else { + ObjectSetInteger(0, rect_name, OBJPROP_TIME, 0, time_left); + ObjectSetDouble(0, rect_name, OBJPROP_PRICE, 0, box_high); + ObjectSetInteger(0, rect_name, OBJPROP_TIME, 1, time_right); + ObjectSetDouble(0, rect_name, OBJPROP_PRICE, 1, box_low); + } + ObjectSetInteger(0, rect_name, OBJPROP_COLOR, clrDodgerBlue); + ObjectSetInteger(0, rect_name, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(0, rect_name, OBJPROP_WIDTH, 1); + ObjectSetInteger(0, rect_name, OBJPROP_FILL, true); + ObjectSetInteger(0, rect_name, OBJPROP_BACK, true); + ObjectSetInteger(0, rect_name, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, rect_name, OBJPROP_HIDDEN, true); + + const string high_line = prefix + "High"; + if(ObjectFind(0, high_line) < 0) + ObjectCreate(0, high_line, OBJ_HLINE, 0, 0, box_high); + else + ObjectSetDouble(0, high_line, OBJPROP_PRICE, box_high); + ObjectSetInteger(0, high_line, OBJPROP_COLOR, clrDodgerBlue); + ObjectSetInteger(0, high_line, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, high_line, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, high_line, OBJPROP_SELECTABLE, false); + + const string low_line = prefix + "Low"; + if(ObjectFind(0, low_line) < 0) + ObjectCreate(0, low_line, OBJ_HLINE, 0, 0, box_low); + else + ObjectSetDouble(0, low_line, OBJPROP_PRICE, box_low); + ObjectSetInteger(0, low_line, OBJPROP_COLOR, clrDodgerBlue); + ObjectSetInteger(0, low_line, OBJPROP_STYLE, STYLE_DASH); + ObjectSetInteger(0, low_line, OBJPROP_WIDTH, 2); + ObjectSetInteger(0, low_line, OBJPROP_SELECTABLE, false); + + const string label_name = prefix + "Label"; + const double mid = (box_high + box_low) * 0.5; + if(ObjectFind(0, label_name) < 0) + ObjectCreate(0, label_name, OBJ_TEXT, 0, time_right, mid); + else { + ObjectSetInteger(0, label_name, OBJPROP_TIME, 0, time_right); + ObjectSetDouble(0, label_name, OBJPROP_PRICE, 0, mid); + } + const double range_pct = (mid > 0.0) ? (box_high - box_low) / mid * 100.0 : 0.0; + ObjectSetString(0, label_name, OBJPROP_TEXT, + StringFormat("箱×%d %.2f%%", active_box.wave_count, range_pct)); + ObjectSetInteger(0, label_name, OBJPROP_COLOR, clrDodgerBlue); + ObjectSetInteger(0, label_name, OBJPROP_FONTSIZE, 8); + ObjectSetInteger(0, label_name, OBJPROP_ANCHOR, ANCHOR_LEFT); + ObjectSetInteger(0, label_name, OBJPROP_SELECTABLE, false); + + // 删除旧箱体对象(保留当前 id) + const int total = ObjectsTotal(0, 0, -1); + for(int i = total - 1; i >= 0; i--) { + const string name = ObjectName(0, i, 0, -1); + if(StringFind(name, "TradeBox_") == 0 && StringFind(name, prefix) != 0) + ObjectDelete(0, name); + } + + ChartRedraw(0); +} + +//+------------------------------------------------------------------+ +//| 绘制所有极值点标记 | +//+------------------------------------------------------------------+ +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; + } + } +} + +//+------------------------------------------------------------------+ +//| 突破挂单管理 | +//+------------------------------------------------------------------+ +long PosTypeOnHighBreakout(const ENUM_BREAKOUT_DIRECTION dir) +{ + if(dir == BREAKOUT_DIR_REVERSE) + return POSITION_TYPE_SELL; + return POSITION_TYPE_BUY; +} + +long PosTypeOnLowBreakout(const ENUM_BREAKOUT_DIRECTION dir) +{ + if(dir == BREAKOUT_DIR_REVERSE) + return POSITION_TYPE_BUY; + return POSITION_TYPE_SELL; +} + +ENUM_ORDER_TYPE PendingTypeOnHighBreakout(const ENUM_BREAKOUT_DIRECTION dir) +{ + if(dir == BREAKOUT_DIR_REVERSE) + return ORDER_TYPE_SELL_LIMIT; + return ORDER_TYPE_BUY_STOP; +} + +ENUM_ORDER_TYPE PendingTypeOnLowBreakout(const ENUM_BREAKOUT_DIRECTION dir) +{ + if(dir == BREAKOUT_DIR_REVERSE) + return ORDER_TYPE_BUY_LIMIT; + return ORDER_TYPE_SELL_STOP; +} + +void CancelEaPendingOrderType(const ENUM_ORDER_TYPE order_type, const string comment_prefix) +{ + const ulong ticket = FindEaPendingOrder(order_type, comment_prefix); + if(ticket > 0) + trade.OrderDelete(ticket); +} + +void CancelLargeWavePendingAtHigh() +{ + CancelEaPendingOrderType(ORDER_TYPE_BUY_STOP, "LW"); + CancelEaPendingOrderType(ORDER_TYPE_SELL_LIMIT, "LW"); +} + +void CancelLargeWavePendingAtLow() +{ + CancelEaPendingOrderType(ORDER_TYPE_SELL_STOP, "LW"); + CancelEaPendingOrderType(ORDER_TYPE_BUY_LIMIT, "LW"); +} + +void CancelBoxPendingAtHigh() +{ + CancelEaPendingOrderType(ORDER_TYPE_BUY_STOP, "BX"); + CancelEaPendingOrderType(ORDER_TYPE_SELL_LIMIT, "BX"); +} + +void CancelBoxPendingAtLow() +{ + CancelEaPendingOrderType(ORDER_TYPE_SELL_STOP, "BX"); + CancelEaPendingOrderType(ORDER_TYPE_BUY_LIMIT, "BX"); +} + +void CancelLargeWavePendingOrders() +{ + CancelLargeWavePendingAtHigh(); + CancelLargeWavePendingAtLow(); +} + +void CancelBoxPendingOrders() +{ + CancelBoxPendingAtHigh(); + CancelBoxPendingAtLow(); +} + +void CancelAllEaPendingOrders() +{ + CancelLargeWavePendingOrders(); + CancelBoxPendingOrders(); +} + +ulong FindEaPendingOrder(const ENUM_ORDER_TYPE order_type, const string comment_prefix) +{ + 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) + continue; + const string comment = OrderGetString(ORDER_COMMENT); + if(StringFind(comment, comment_prefix) == 0) + return ticket; + } + return 0; +} + +bool CalcPendingSlTp(const ENUM_ORDER_TYPE pending_type, const double trigger_price, + const int sl_points, const int tp_points, double &sl, double &tp) +{ + const double stop_loss_amount = StopLossOffsetPoints(sl_points); + const double take_profit_amount = TakeProfitOffsetPoints(tp_points); + 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 || pending_type == ORDER_TYPE_BUY_LIMIT) { + 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 || pending_type == ORDER_TYPE_SELL_LIMIT) { + 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 int sl_points, const int tp_points, + const string comment) +{ + const double lots = CalculateLotSize(sl_points); + if(lots <= 0.0) + return false; + + double sl = 0.0, tp = 0.0; + if(!CalcPendingSlTp(pending_type, trigger_price, sl_points, tp_points, sl, tp)) + return false; + + trade.SetExpertMagicNumber(InpMagicNumber); + + 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); + else if(pending_type == ORDER_TYPE_BUY_LIMIT) + result = trade.BuyLimit(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + else if(pending_type == ORDER_TYPE_SELL_LIMIT) + result = trade.SellLimit(lots, trigger_price, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment); + + if(!result) { + Print("挂单失败 ", comment, " ", EnumToString(pending_type), + " 错误:", trade.ResultRetcode()); + return false; + } + return true; +} + +void SyncPendingAtExtreme(const bool at_high, const double trigger_price, + const double range_high, const double range_low, + const ENUM_BREAKOUT_DIRECTION dir, const int sl_points, + const int tp_points, const string comment_prefix) +{ + const ENUM_ORDER_TYPE pending_type = at_high ? + PendingTypeOnHighBreakout(dir) : PendingTypeOnLowBreakout(dir); + const ENUM_ORDER_TYPE stale_type = at_high ? + (pending_type == ORDER_TYPE_BUY_STOP ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_BUY_STOP) : + (pending_type == ORDER_TYPE_SELL_STOP ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_SELL_STOP); + CancelEaPendingOrderType(stale_type, comment_prefix); + + const double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); + const bool price_crossed = at_high ? + (ask >= trigger_price - _Point * 0.5) : + (bid <= trigger_price + _Point * 0.5); + + if(price_crossed) { + if(StringCompare(comment_prefix, "LW") == 0) { + if(at_high) + CancelLargeWavePendingAtHigh(); + else + CancelLargeWavePendingAtLow(); + } else { + if(at_high) + CancelBoxPendingAtHigh(); + else + CancelBoxPendingAtLow(); + } + return; + } + + const int range_pts = (int)MathRound(MathAbs(range_high - range_low) / _Point); + const double pending_price = NormalizeDouble(trigger_price, _Digits); + const string comment = StringFormat("%s%d@%.*f", comment_prefix, range_pts, _Digits, pending_price); + + double expected_sl = 0.0, expected_tp = 0.0; + if(!CalcPendingSlTp(pending_type, trigger_price, sl_points, tp_points, expected_sl, expected_tp)) + return; + + ulong ticket = FindEaPendingOrder(pending_type, comment_prefix); + if(ticket > 0 && OrderSelect(ticket)) { + const double existing_price = OrderGetDouble(ORDER_PRICE_OPEN); + const double lots = CalculateLotSize(sl_points); + if(MathAbs(existing_price - trigger_price) > _Point * 0.5 || + MathAbs(OrderGetDouble(ORDER_VOLUME_CURRENT) - lots) > 1e-8 || + MathAbs(OrderGetDouble(ORDER_SL) - expected_sl) > _Point * 0.5 || + MathAbs(OrderGetDouble(ORDER_TP) - expected_tp) > _Point * 0.5) { + trade.OrderDelete(ticket); + ticket = 0; + } + } + if(ticket == 0) + PlaceBreakoutPendingOrder(pending_type, trigger_price, sl_points, tp_points, comment); +} + +void SyncLargeWaveBreakoutPendingOrders() +{ + if(!IsLargeWaveTradeEligible()) { + CancelLargeWavePendingOrders(); + g_sync_wave_high = 0.0; + g_sync_wave_low = 0.0; + return; + } + + 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) { + CancelLargeWavePendingOrders(); + g_sync_wave_high = latest_wave.high_price; + g_sync_wave_low = latest_wave.low_price; + } + + const double wh = latest_wave.high_price; + const double wl = latest_wave.low_price; + + if(!latest_wave.high_used) { + SyncPendingAtExtreme(true, wh, wh, wl, InpLargeWaveDirection, + InpLargeWaveStopLossPoints, InpLargeWaveTakeProfitPoints, "LW"); + } else { + CancelLargeWavePendingAtHigh(); + } + + if(!latest_wave.low_used) { + SyncPendingAtExtreme(false, wl, wh, wl, InpLargeWaveDirection, + InpLargeWaveStopLossPoints, InpLargeWaveTakeProfitPoints, "LW"); + } else { + CancelLargeWavePendingAtLow(); + } +} + +void SyncBoxBreakoutPendingOrders() +{ + if(!InpEnableBoxTrade || !active_box.exists) { + CancelBoxPendingOrders(); + g_sync_box_high = 0.0; + g_sync_box_low = 0.0; + return; + } + + if(MathAbs(active_box.high_price - g_sync_box_high) > _Point * 0.5 || + MathAbs(active_box.low_price - g_sync_box_low) > _Point * 0.5) { + CancelBoxPendingOrders(); + g_sync_box_high = active_box.high_price; + g_sync_box_low = active_box.low_price; + } + + const double bh = active_box.high_price; + const double bl = active_box.low_price; + + if(!active_box.high_used) { + SyncPendingAtExtreme(true, bh, bh, bl, InpBoxDirection, + InpBoxStopLossPoints, InpBoxTakeProfitPoints, "BX"); + } else { + CancelBoxPendingAtHigh(); + } + + if(!active_box.low_used) { + SyncPendingAtExtreme(false, bl, bh, bl, InpBoxDirection, + InpBoxStopLossPoints, InpBoxTakeProfitPoints, "BX"); + } else { + CancelBoxPendingAtLow(); + } +} + +//+------------------------------------------------------------------+ +//| 止损/止盈距离(点数) | +//+------------------------------------------------------------------+ +double StopLossOffsetPoints(const int sl_points) +{ + if(sl_points <= 0) + return 0.0; + return (double)sl_points * _Point; +} + +double TakeProfitOffsetPoints(const int tp_points) +{ + if(tp_points <= 0) + return 0.0; + return (double)tp_points * _Point; +} + +int StopLossPointsFromComment(const string &comment) +{ + if(StringFind(comment, "BX") == 0) + return InpBoxStopLossPoints; + return InpLargeWaveStopLossPoints; +} + +bool UseTrailingStopFromComment(const string &comment) +{ + if(StringFind(comment, "BX") == 0) + return InpBoxUseTrailingStop; + return InpLargeWaveUseTrailingStop; +} + +//+------------------------------------------------------------------+ +//| 手数规范化 | +//+------------------------------------------------------------------+ +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 int sl_points) +{ + double lots = InpFixedLots; + + if(InpLotSizeMode == LOT_SIZE_RISK_PERCENT && InpRiskPercent > 0.0) { + const double stop_loss_dist = StopLossOffsetPoints(sl_points); + 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; + + CheckTrailingStop(PositionGetTicket(i)); + } +} + +//+------------------------------------------------------------------+ +//| 检查移动止损 | +//+------------------------------------------------------------------+ +void CheckTrailingStop(ulong ticket) +{ + if(!PositionSelectByTicket(ticket)) + return; + + const string comment = PositionGetString(POSITION_COMMENT); + if(!UseTrailingStopFromComment(comment)) + 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); + + const double stop_loss_amount = StopLossOffsetPoints(StopLossPointsFromComment(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()); + } + } + } +} diff --git a/20260526-突破-箱体/README.md b/20260526-突破-箱体/README.md new file mode 100644 index 0000000..5a7611a --- /dev/null +++ b/20260526-突破-箱体/README.md @@ -0,0 +1,21 @@ +# 20260526 大波段 + 聚合箱体突破 EA + +## 逻辑概要 + +1. **波段识别(结构)**:MA 突破 → 极值 → 有效波段(0.1%~2%,最少 3 根 K)。`InpPullbackTolerance` 默认 0.05%,仅影响结构识别时是否切断波段。 + +2. **大波段突破**:最新有效波段振幅 **>0.25% 且 ≤2%** 时,在波段高/低挂突破单。备注 `LW{振幅点数}@{挂单价}`(成交后可对比成交价算滑点)。独立 SL/TP/移动止损/止损再挂。 + +3. **聚合箱体**:从最新向旧连续纳入振幅 **0.1%~0.25%** 的小波段,**1~3** 段成箱(超出 3 段则只保留最近 3 段),在箱沿挂突破单。备注 `BX{振幅点数}@{挂单价}`。独立 SL/TP/移动止损/止损再挂。 + +两套机制 **同时运行、互不影响**(同一 `InpMagicNumber`,靠订单备注区分)。 + +4. **交易时段(北京时间 UTC+8)**:默认 **关闭**;启用后按 `可交易时段`(如 `15:00-02:00`)才挂突破单,时段外撤销全部 EA 挂单,持仓照常管理。多段用逗号分隔。 + +## 代码参数对照 + +见 `参数设置.md`;`20260526_box_breakout.mq5` 中 input 默认值已与该文档对齐。 + +## 已移除(参数文档未定义) + +回落反向、连续亏损冷冻、最大持仓笔数、单方向限仓、单箱仅一单、箱体合并包络上限、双 Magic 偏移、调试开关等。 diff --git a/20260526-突破-箱体/参数设置.md b/20260526-突破-箱体/参数设置.md new file mode 100644 index 0000000..f0f5667 --- /dev/null +++ b/20260526-突破-箱体/参数设置.md @@ -0,0 +1,52 @@ +## 波段识别参数 + +- K线周期:1min +- MA周期:14 +- 最小波段振幅百分比:0.1 +- 最大波段阈值百分比:2 +- 反向突破容忍度:0.05 +- 有效波段最少K线数:3 + +## 大波段突破机制参数 + +- 启用大波段突破机制:true +- 开仓方向:顺势 +- 最小波段振幅百分比(>该值):0.25 +- 最大波段振幅百分比(<=该值):2 +- 边界止损出局后是否允许再挂一单:false +- 止损点数:80 +- 止盈点数:120 +- 使用移动止损:false + +## 聚合箱体机制参数 + +- 启用聚合箱体突破机制:true +- 开仓方向:顺势 +- 成箱最小波段振幅百分比(>=该值):0.1 +- 成箱最大波段振幅百分比(<=该值):0.25 +- 成箱最少有效波段数:1 +- 成箱最多有效波段数:3 +- 边界止损出局后是否允许再挂一单:false +- 止损点数:80 +- 止盈点数:120 +- 使用移动止损:false + +## 仓位管理模式 + +- 仓位管理模式:RiskPercent模式 +- 固定手数(固定手数模式):0.01 +- 每笔风险占结余%(RiskPercent模式): 5 +- 单笔最大手数:10 + +## 其他参数 + +- 禁止手工单:true +- EA魔术号:20260526 +- 显示极值点标记:false +- 显示箱体标记:false +- 显示最近有效波段标记:false + +## 交易时段(北京时间) + +- 启用时段过滤:false(时段外撤掉本 EA 全部挂单;已有持仓仍管理 SL/TP/移动止损) +- 可交易时段:15:00-02:00(跨日:当天 15:00 至次日 02:00 前;02:00 起不可交易至 15:00)