Add range breakout guard for reverse limit orders

This commit is contained in:
Hiroaki86
2026-06-13 11:27:10 +09:00
parent 2532d416fa
commit 4de3d24dcd
5 changed files with 414 additions and 2 deletions
+13
View File
@@ -453,6 +453,13 @@ int TrySendSplitEntryOrders(const int orderType, EAState &state, TickContext &ct
{
string entry_type = EntryTypeName(orderType);
double cur_price = CurrentPriceForOrderType(orderType, ctx);
string guard_reason = "";
if(IsLimitOrderBlockedByRangeBreakoutGuard(orderType, guard_reason))
{
Print("[Split Skip] ", entry_type, " blocked by range breakout guard. ", guard_reason);
return 0;
}
double zone_low = state.zone_low[orderType];
double zone_high = state.zone_high[orderType];
double tp = state.zone_tp[orderType];
@@ -551,6 +558,12 @@ bool TrySendEntryOrder(const int orderType, EAState &state, TickContext &ctx)
{
string entry_type = EntryTypeName(orderType);
double cur_price = CurrentPriceForOrderType(orderType, ctx);
string guard_reason = "";
if(IsLimitOrderBlockedByRangeBreakoutGuard(orderType, guard_reason))
{
Print("[Skip] ", entry_type, " blocked by range breakout guard. ", guard_reason);
return false;
}
double en = state.en_price[orderType];
double tp = state.tp_price[orderType];
+334
View File
@@ -599,6 +599,340 @@ bool CancelPendingOrdersNotAllowedByTrend(const int trend_state)
return result;
}
//+------------------------------------------------------------------+
//| レンジブレイクガード用の入力値を安全な範囲へ丸める関数群
//+------------------------------------------------------------------+
int EffectiveRangeBreakoutLookbackBars()
{
int bars = input_range_breakout_lookback_bars;
if(bars < 5)
bars = 5;
if(bars > 200)
bars = 200;
return bars;
}
double EffectiveRangeBreakoutMaxWidthMultiplier()
{
double multiplier = input_range_breakout_max_width_multiplier;
if(multiplier < 0.0)
multiplier = 0.0;
if(multiplier > 100.0)
multiplier = 100.0;
return multiplier;
}
double EffectiveRangeBreakoutBufferMultiplier()
{
double multiplier = input_range_breakout_buffer_multiplier;
if(multiplier <= 0.0)
multiplier = 0.20;
if(multiplier > 10.0)
multiplier = 10.0;
return multiplier;
}
double EffectiveRangeBreakoutNearMultiplier()
{
double multiplier = input_range_breakout_near_multiplier;
if(multiplier <= 0.0)
multiplier = 0.50;
if(multiplier > 20.0)
multiplier = 20.0;
return multiplier;
}
double EffectiveRangeBreakoutBodyMultiplier()
{
double multiplier = input_range_breakout_body_multiplier;
if(multiplier <= 0.0)
multiplier = 1.50;
if(multiplier > 20.0)
multiplier = 20.0;
return multiplier;
}
int EffectiveRangeBreakoutCooldownBars()
{
int bars = input_range_breakout_cooldown_bars;
if(bars < 1)
bars = 1;
if(bars > 96)
bars = 96;
return bars;
}
//+------------------------------------------------------------------+
//| レンジブレイク方向の表示名
//+------------------------------------------------------------------+
string RangeBreakoutDirectionName(const int direction)
{
if(direction == RANGE_BREAKOUT_UP)
return "UP";
if(direction == RANGE_BREAKOUT_DOWN)
return "DOWN";
return "NONE";
}
//+------------------------------------------------------------------+
//| 価格を現在シンボルの桁数で表示する関数
//+------------------------------------------------------------------+
string RangeBreakoutPriceText(const double price)
{
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
return DoubleToString(price, digits);
}
//+------------------------------------------------------------------+
//| 期限切れのレンジブレイクガード状態を解除する関数
//+------------------------------------------------------------------+
void ExpireRangeBreakoutLimitGuard()
{
if(g_range_breakout_guard_direction == RANGE_BREAKOUT_NONE)
return;
if(g_range_breakout_guard_until <= 0)
return;
if(TimeCurrent() < g_range_breakout_guard_until)
return;
Print("[Range Breakout Guard] cooldown expired. direction=",
RangeBreakoutDirectionName(g_range_breakout_guard_direction));
g_range_breakout_guard_direction = RANGE_BREAKOUT_NONE;
g_range_breakout_guard_until = 0;
}
//+------------------------------------------------------------------+
//| レンジブレイク状態を有効化する関数
//+------------------------------------------------------------------+
void ActivateRangeBreakoutLimitGuard(const int direction, const string reason)
{
if(direction == RANGE_BREAKOUT_NONE)
return;
int timeframe_seconds = PeriodSeconds(input_range_breakout_guard_timeframe);
if(timeframe_seconds <= 0)
timeframe_seconds = PeriodSeconds(_Period);
if(timeframe_seconds <= 0)
timeframe_seconds = 60;
datetime new_until = TimeCurrent() + timeframe_seconds * EffectiveRangeBreakoutCooldownBars();
if(g_range_breakout_guard_direction == direction &&
g_range_breakout_guard_until > TimeCurrent())
return;
g_range_breakout_guard_direction = direction;
g_range_breakout_guard_until = new_until;
Print("[Range Breakout Guard] activated. direction=", RangeBreakoutDirectionName(direction),
" until=", TimeToString(g_range_breakout_guard_until, TIME_DATE | TIME_SECONDS),
" reason=", reason);
}
//+------------------------------------------------------------------+
//| レンジブレイクを分析し、逆張りLimitを止める方向を返す関数
//+------------------------------------------------------------------+
bool AnalyzeRangeBreakoutLimitGuard(TickContext &ctx, int &direction, string &reason)
{
direction = RANGE_BREAKOUT_NONE;
reason = "";
if(!input_range_breakout_guard_enabled)
return false;
int lookback = EffectiveRangeBreakoutLookbackBars();
MqlRates rates[];
int copied = CopyRates(_Symbol, input_range_breakout_guard_timeframe, 1, lookback + 1, rates);
if(copied < lookback + 1)
return false;
int last_index = copied - 1;
int history_count = copied - 1;
if(history_count < lookback || last_index <= 0)
return false;
bool initialized = false;
double range_high = 0.0;
double range_low = 0.0;
double total_range = 0.0;
double total_body = 0.0;
// Build the reference range from closed bars before the latest closed bar.
for(int i = 0; i < history_count; i++)
{
if(!initialized)
{
range_high = rates[i].high;
range_low = rates[i].low;
initialized = true;
}
else
{
if(rates[i].high > range_high)
range_high = rates[i].high;
if(rates[i].low < range_low)
range_low = rates[i].low;
}
total_range += rates[i].high - rates[i].low;
total_body += MathAbs(rates[i].close - rates[i].open);
}
if(!initialized)
return false;
double avg_range = total_range / history_count;
double avg_body = total_body / history_count;
if(avg_range <= 0.0)
avg_range = Point();
if(avg_body <= 0.0)
avg_body = Point();
double range_width = range_high - range_low;
if(range_width <= 0.0)
return false;
double max_width_multiplier = EffectiveRangeBreakoutMaxWidthMultiplier();
if(max_width_multiplier > 0.0 && range_width > avg_range * max_width_multiplier)
return false;
MqlRates last_bar = rates[last_index];
double buffer = MathMax(avg_range * EffectiveRangeBreakoutBufferMultiplier(), ctx.spread);
double near_buffer = MathMax(avg_range * EffectiveRangeBreakoutNearMultiplier(), ctx.spread);
double body_threshold = avg_body * EffectiveRangeBreakoutBodyMultiplier();
double last_body = MathAbs(last_bar.close - last_bar.open);
bool last_bullish = (last_bar.close > last_bar.open);
bool last_bearish = (last_bar.close < last_bar.open);
bool last_large_body = (last_body >= body_threshold);
bool confirmed_up = (last_bar.close > range_high + buffer);
bool confirmed_down = (last_bar.close < range_low - buffer);
bool warning_up = (ctx.bid >= range_high - near_buffer &&
(confirmed_up || ctx.bid > range_high + buffer ||
(last_bullish && (last_large_body || last_bar.close > range_high))));
bool warning_down = (ctx.ask <= range_low + near_buffer &&
(confirmed_down || ctx.ask < range_low - buffer ||
(last_bearish && (last_large_body || last_bar.close < range_low))));
if(confirmed_up || warning_up)
{
direction = RANGE_BREAKOUT_UP;
reason = "upward " + string(confirmed_up ? "confirmed" : "warning") +
" tf=" + EnumToString(input_range_breakout_guard_timeframe) +
" range=[" + RangeBreakoutPriceText(range_low) + "," +
RangeBreakoutPriceText(range_high) + "] bid=" +
RangeBreakoutPriceText(ctx.bid) + " last_close=" +
RangeBreakoutPriceText(last_bar.close);
return true;
}
if(confirmed_down || warning_down)
{
direction = RANGE_BREAKOUT_DOWN;
reason = "downward " + string(confirmed_down ? "confirmed" : "warning") +
" tf=" + EnumToString(input_range_breakout_guard_timeframe) +
" range=[" + RangeBreakoutPriceText(range_low) + "," +
RangeBreakoutPriceText(range_high) + "] ask=" +
RangeBreakoutPriceText(ctx.ask) + " last_close=" +
RangeBreakoutPriceText(last_bar.close);
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| レンジブレイク方向と逆向きのLimit pendingを取消する関数
//+------------------------------------------------------------------+
bool CancelRangeBreakoutOppositeLimitOrders(const int direction, const string reason)
{
if(direction == RANGE_BREAKOUT_NONE)
return false;
bool result = false;
CleanupPendingOrderCancelAttempts();
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0 || !OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
ENUM_ORDER_TYPE order_type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
bool should_cancel = ((direction == RANGE_BREAKOUT_UP && order_type == ORDER_TYPE_SELL_LIMIT) ||
(direction == RANGE_BREAKOUT_DOWN && order_type == ORDER_TYPE_BUY_LIMIT));
if(!should_cancel)
continue;
if(TryCancelPendingOrder(ticket, "range breakout guard: " + reason))
result = true;
}
return result;
}
//+------------------------------------------------------------------+
//| レンジブレイクガードを監視し、逆方向Limitを退避する関数
//+------------------------------------------------------------------+
void ManageRangeBreakoutLimitGuard(TickContext &ctx)
{
if(!input_range_breakout_guard_enabled)
return;
ExpireRangeBreakoutLimitGuard();
int direction = RANGE_BREAKOUT_NONE;
string reason = "";
if(AnalyzeRangeBreakoutLimitGuard(ctx, direction, reason))
ActivateRangeBreakoutLimitGuard(direction, reason);
if(g_range_breakout_guard_direction == RANGE_BREAKOUT_NONE ||
g_range_breakout_guard_until <= TimeCurrent())
return;
string active_reason = "active " +
RangeBreakoutDirectionName(g_range_breakout_guard_direction) +
" cooldown until " +
TimeToString(g_range_breakout_guard_until, TIME_DATE | TIME_SECONDS);
CancelRangeBreakoutOppositeLimitOrders(g_range_breakout_guard_direction, active_reason);
}
//+------------------------------------------------------------------+
//| 新規Limit注文がレンジブレイクガードで停止中か判定する関数
//+------------------------------------------------------------------+
bool IsLimitOrderBlockedByRangeBreakoutGuard(const int orderType, string &reason)
{
reason = "";
if(!input_range_breakout_guard_enabled)
return false;
ExpireRangeBreakoutLimitGuard();
if(g_range_breakout_guard_direction == RANGE_BREAKOUT_NONE ||
g_range_breakout_guard_until <= TimeCurrent())
return false;
bool blocked = ((g_range_breakout_guard_direction == RANGE_BREAKOUT_UP && orderType == 4) ||
(g_range_breakout_guard_direction == RANGE_BREAKOUT_DOWN && orderType == 2));
if(!blocked)
return false;
reason = "direction=" + RangeBreakoutDirectionName(g_range_breakout_guard_direction) +
" until=" + TimeToString(g_range_breakout_guard_until, TIME_DATE | TIME_SECONDS);
return true;
}
//+------------------------------------------------------------------+
//| pendingコメントからH1候補時刻を復元する関数
//+------------------------------------------------------------------+