Files
EA_with_Python/Include/MyLib/Trading/HITTradeManager.mqh
2026-06-14 22:02:17 +09:00

1072 lines
36 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#ifndef HIT_TRADE_MANAGER_MQH
#define HIT_TRADE_MANAGER_MQH
//+------------------------------------------------------------------+
//| 注文ロットがブローカー制約を満たすか判定する関数
//+------------------------------------------------------------------+
/**
* @brief 通常注文と分割注文の最終送信前に volume min/max/step を検証します。
*/
bool IsOrderVolumeAllowed(const double volume, const string context)
{
double min_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double max_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double step_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(volume <= 0.0 || min_volume <= 0.0 || max_volume <= 0.0 || step_volume <= 0.0)
{
Print("[Order Skip] invalid volume setting. context=", context,
" volume=", volume, " min=", min_volume,
" max=", max_volume, " step=", step_volume);
return false;
}
if(volume < min_volume || volume > max_volume)
{
Print("[Order Skip] volume out of range. context=", context,
" volume=", volume, " min=", min_volume, " max=", max_volume);
return false;
}
double steps = (volume - min_volume) / step_volume;
double nearest = MathRound(steps);
if(MathAbs(steps - nearest) > 0.000001)
{
Print("[Order Skip] volume does not match broker step. context=", context,
" volume=", volume, " min=", min_volume, " step=", step_volume);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| ブローカーのロットstepから正規化桁数を推定する関数
//+------------------------------------------------------------------+
int VolumeDigits()
{
int volume_digits = 2;
double step_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(step_volume > 0.0)
{
volume_digits = 0;
double step = step_volume;
while(step < 1.0 && volume_digits < 8)
{
step *= 10.0;
volume_digits++;
}
}
return volume_digits;
}
//+------------------------------------------------------------------+
//| エントリー注文を送信する関数 (1:buy-stop, 2:buy-limit, 3:sell-stop, 4:sell-limit)
//+------------------------------------------------------------------+
/**
* @brief 指定された注文タイプでペンディング注文を送信します。
*
* @param orderType 注文タイプ。1=Buy Stop、2=Buy Limit、3=Sell Stop、4=Sell Limit。
* @param price エントリー価格。
* @param tp 利確価格。
* @param sl 損切価格。
* @param volume 注文ロット。
* @param comment_suffix H1候補または分割注文識別用のコメント接尾辞。空なら従来コメント。
* @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。
* @return 注文が正常に受理された場合はtrue、それ以外はfalse。
*/
bool SendOrder(int orderType, double price, double tp, double sl, double volume,
string comment_suffix, datetime candidate_reference_time)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.magic = magic_number;
request.symbol = _Symbol;
request.volume = NormalizeDouble(volume, VolumeDigits());
request.deviation = slippage;
request.type_filling = GetOrderFillingPolicy(_Symbol);
request.price = NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.tp = NormalizeDouble(tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.sl = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.comment = RequestComment(orderType, comment_suffix);
string orderTypeStr = "";
// 指値または逆指値注文
request.action = TRADE_ACTION_PENDING;
if(!ApplyPendingOrderExpiration(request, candidate_reference_time))
return false;
switch(orderType)
{
case 1: // 順張り買い (buy-stop)
request.type = ORDER_TYPE_BUY_STOP;
orderTypeStr = "buy-stop";
break;
case 2: // 逆張り買い (buy-limit)
request.type = ORDER_TYPE_BUY_LIMIT;
orderTypeStr = "buy-limit";
break;
case 3: // 順張り売り (sell-stop)
request.type = ORDER_TYPE_SELL_STOP;
orderTypeStr = "sell-stop";
break;
case 4: // 逆張り売り (sell-limit)
request.type = ORDER_TYPE_SELL_LIMIT;
orderTypeStr = "sell-limit";
break;
default: // 不正なorderType
Print(__FUNCTION__, ": invalid orderType=", orderType);
return false;
}
string volume_context = orderTypeStr;
if(comment_suffix != "")
volume_context += " " + comment_suffix;
if(!IsOrderVolumeAllowed(volume, volume_context))
return false;
// 注文送信
if(!OrderSend(request, result))
{
Print(__FUNCTION__, ": OrderSend failed. err=", GetLastError());
return false;
}
// 注文成功確認
if(result.retcode == TRADE_RETCODE_PLACED ||
result.retcode == TRADE_RETCODE_DONE ||
result.retcode == TRADE_RETCODE_DONE_PARTIAL)
{
Print(request.comment + " - " + orderTypeStr + " order success (ticket=", result.order, ")");
return true;
}
else
{
Print(__FUNCTION__, ": OrderSend retcode=", result.retcode);
return false;
}
}
//+------------------------------------------------------------------+
//| 注文の執行ポリシーを取得する関数
//+------------------------------------------------------------------+
/**
* @brief シンボルに対応する注文執行ポリシーを取得します。
*
* @param symbol 対象シンボル。
* @return 利用可能な執行ポリシー。IOC、FOK、RETURNの順で選択します。
*/
ENUM_ORDER_TYPE_FILLING GetOrderFillingPolicy(string symbol)
{
long fill_mode = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((fill_mode & SYMBOL_FILLING_IOC) != 0)
return ORDER_FILLING_IOC;
if((fill_mode & SYMBOL_FILLING_FOK) != 0)
return ORDER_FILLING_FOK;
return ORDER_FILLING_RETURN;
}
//+------------------------------------------------------------------+
//| ペンディング注文の有効期限を設定する関数
//+------------------------------------------------------------------+
/**
* @brief brokerが対応している範囲でpending orderにサーバー側の期限を設定します。
*
* @param request 更新対象の取引リクエスト。
* @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。
* @return H1候補の期限が残っている場合はtrue。発注前に期限切れならfalse。
*/
bool ApplyPendingOrderExpiration(MqlTradeRequest &request, const datetime candidate_reference_time)
{
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
datetime expiration_at = TimeCurrent() + expiration_seconds;
if(candidate_reference_time > 0)
expiration_at = candidate_reference_time + expiration_seconds;
if(expiration_seconds <= 0 || expiration_at <= TimeCurrent())
{
Print("[Order Skip] H1 candidate expiration is not in the future. candidate_at=",
TimeToString(candidate_reference_time, TIME_DATE | TIME_SECONDS),
" expiration_at=", TimeToString(expiration_at, TIME_DATE | TIME_SECONDS));
return false;
}
long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE);
if((expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0)
{
request.type_time = ORDER_TIME_SPECIFIED;
request.expiration = expiration_at;
return true;
}
if((expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0)
{
request.type_time = ORDER_TIME_SPECIFIED_DAY;
request.expiration = expiration_at;
return true;
}
if((expiration_mode & SYMBOL_EXPIRATION_DAY) != 0)
{
request.type_time = ORDER_TIME_DAY;
return true;
}
request.type_time = ORDER_TIME_GTC;
return true;
}
//+------------------------------------------------------------------+
//| 注文コメント(ローカル時刻)を生成する関数(先頭に orderType を付与)
//+------------------------------------------------------------------+
/**
* @brief 注文コメント文字列を生成します。
*
* @param orderType 注文タイプ番号。
* @param comment_suffix 分割注文識別用のコメント接尾辞。
* @return 注文タイプとPCローカル時刻、または分割注文識別子を含むコメント文字列。
*/
string RequestComment(int orderType, string comment_suffix)
{
if(comment_suffix != "")
return "T" + IntegerToString(orderType) + " " + comment_suffix;
datetime localTime = TimeLocal();
string timeStr = TimeToString(localTime, TIME_DATE|TIME_MINUTES);
// 先頭に 1,2,3,4 を付ける(例: "1 | PC Time: 2026.02.14 19:05"
return IntegerToString(orderType) + " | PC Time: " + timeStr;
}
//+------------------------------------------------------------------+
//| H4 market_stateに対して許可された注文タイプか判定する関数
//+------------------------------------------------------------------+
/**
* @brief H4 market_stateに対して指定注文タイプが許可されるか判定します。
*
* @param orderType 注文タイプ番号。
* @param trend_state H4 market_state0..5、6は技術エラー停止)。
* @return 許可される注文タイプならtrue。技術エラー停止または異常値ではfalse。
*/
bool IsOrderTypeAllowedByTrend(const int orderType, const int trend_state)
{
switch(trend_state)
{
case MARKET_LOW_VOL_RANGE:
case MARKET_HIGH_VOL_RANGE:
return (orderType == 2 || orderType == 4); // Range: Buy Limit / Sell Limitのみ
case MARKET_LOW_VOL_UP:
case MARKET_HIGH_VOL_UP:
return (orderType == 1 || orderType == 2); // Up: Buy Stop / Buy Limitのみ
case MARKET_LOW_VOL_DOWN:
case MARKET_HIGH_VOL_DOWN:
return (orderType == 3 || orderType == 4); // Down: Sell Stop / Sell Limitのみ
case MARKET_TECHNICAL_ERROR_STOP:
default:
return false; // 技術エラー停止または異常値は新規注文しない
}
}
//+------------------------------------------------------------------+
//| target_prices.txt の価格が有効か判定する関数
//+------------------------------------------------------------------+
/**
* @brief エントリー価格、利確価格、損切価格が有効値か判定します。
*
* @param en エントリー価格。
* @param tp 利確価格。
* @param sl 損切価格。
* @return すべて0より大きい場合はtrue。
*/
bool HasValidTargetPrices(const double en, const double tp, const double sl)
{
return (en > 0.0 && tp > 0.0 && sl > 0.0);
}
//+------------------------------------------------------------------+
//| 自EAの未約定注文数を数える関数
//+------------------------------------------------------------------+
/**
* @brief このEAが管理対象とする未約定注文数を数えます。
*
* @return `_Symbol` と `magic_number` が一致する未約定注文数。
*/
int CountMyPendingOrders()
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0)
continue;
if(!OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
count++;
}
return count;
}
//+------------------------------------------------------------------+
//| 自EAのポジション数を数える関数
//+------------------------------------------------------------------+
/**
* @brief このEAが管理対象とする保有ポジション数を数えます。
*
* @return `_Symbol` と `magic_number` が一致するポジション数。
*/
int CountMyPositions()
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string symbol = PositionGetSymbol(i);
if(symbol == "")
continue;
if(symbol != _Symbol)
continue;
if((int)PositionGetInteger(POSITION_MAGIC) != magic_number)
continue;
count++;
}
return count;
}
//+------------------------------------------------------------------+
//| 自EAの注文+ポジション数を数える関数
//+------------------------------------------------------------------+
/**
* @brief このEAが使用中の注文数とポジション数の合計を返します。
*
* @return 未約定注文数 + 保有ポジション数。
*/
int CountMyUsed()
{
return CountMyPendingOrders() + CountMyPositions();
}
//+------------------------------------------------------------------+
//| 実効ポジション上限を返す関数
//+------------------------------------------------------------------+
/**
* @brief input指定の上限を安全な範囲に丸めて返します。
*/
int EffectivePositionLimit()
{
int limit = input_position_limit;
if(limit < 1)
limit = 1;
if(limit > POSITION_LIMIT)
limit = POSITION_LIMIT;
return limit;
}
//+------------------------------------------------------------------+
//| 分割注文コメントか判定する関数
//+------------------------------------------------------------------+
bool IsSplitOrderComment(const string comment)
{
return (StringFind(comment, " Z") >= 0 && StringFind(comment, "#") >= 0);
}
//+------------------------------------------------------------------+
//| 指定候補IDの分割注文コメントか判定する関数
//+------------------------------------------------------------------+
bool IsCurrentSplitCandidateComment(const string comment, const string candidate_id)
{
if(candidate_id == "" || candidate_id == "0")
return false;
return (StringFind(comment, "Z" + candidate_id + "#") >= 0);
}
//+------------------------------------------------------------------+
//| pending取消のticket単位クールダウン管理
//+------------------------------------------------------------------+
// Prevent repeated server-side cancel requests for the same pending ticket.
int EffectiveCancelRetryCooldownSeconds()
{
int cooldown = input_cancel_retry_cooldown_seconds;
if(cooldown < 1)
cooldown = 1;
if(cooldown > 3600)
cooldown = 3600;
return cooldown;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int FindPendingOrderCancelAttempt(const ulong ticket)
{
for(int i = ArraySize(g_cancel_retry_tickets) - 1; i >= 0; i--)
{
if(g_cancel_retry_tickets[i] == ticket)
return i;
}
return -1;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void RemovePendingOrderCancelAttemptAt(const int index)
{
int size = ArraySize(g_cancel_retry_tickets);
if(index < 0 || index >= size)
return;
int last = size - 1;
if(index != last)
{
g_cancel_retry_tickets[index] = g_cancel_retry_tickets[last];
g_cancel_retry_at[index] = g_cancel_retry_at[last];
}
ArrayResize(g_cancel_retry_tickets, last);
ArrayResize(g_cancel_retry_at, last);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void ForgetPendingOrderCancelAttempt(const ulong ticket)
{
RemovePendingOrderCancelAttemptAt(FindPendingOrderCancelAttempt(ticket));
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CanAttemptPendingOrderCancel(const ulong ticket)
{
int index = FindPendingOrderCancelAttempt(ticket);
if(index < 0)
return true;
return (TimeCurrent() - g_cancel_retry_at[index] >= EffectiveCancelRetryCooldownSeconds());
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void MarkPendingOrderCancelAttempt(const ulong ticket)
{
int index = FindPendingOrderCancelAttempt(ticket);
if(index >= 0)
{
g_cancel_retry_at[index] = TimeCurrent();
return;
}
int size = ArraySize(g_cancel_retry_tickets);
ArrayResize(g_cancel_retry_tickets, size + 1);
ArrayResize(g_cancel_retry_at, size + 1);
g_cancel_retry_tickets[size] = ticket;
g_cancel_retry_at[size] = TimeCurrent();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CleanupPendingOrderCancelAttempts()
{
for(int i = ArraySize(g_cancel_retry_tickets) - 1; i >= 0; i--)
{
if(!OrderSelect(g_cancel_retry_tickets[i]))
RemovePendingOrderCancelAttemptAt(i);
}
}
//+------------------------------------------------------------------+
//| pending取消要求を共通化する関数
//+------------------------------------------------------------------+
/**
* @brief 同一ticketへの取消要求をクールダウンし、取引サーバーへの過剰送信を防ぎます。
*/
bool TryCancelPendingOrder(const ulong ticket, const string reason)
{
if(ticket == 0 || !CanAttemptPendingOrderCancel(ticket))
return false;
MarkPendingOrderCancelAttempt(ticket);
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
request.action = TRADE_ACTION_REMOVE;
request.order = ticket;
ResetLastError();
if(!OrderSend(request, trade_result))
{
Print("Failed to delete order. Ticket: ", ticket,
" Reason: ", reason, " Error: ", GetLastError(),
" RetryAfterSeconds: ", EffectiveCancelRetryCooldownSeconds());
return false;
}
if(trade_result.retcode == TRADE_RETCODE_DONE)
{
Print("Pending order canceled. Ticket: ", ticket, " Reason: ", reason);
ForgetPendingOrderCancelAttempt(ticket);
return true;
}
Print("Order cancellation failed. Ticket: ", ticket,
" Reason: ", reason, " Retcode: ", trade_result.retcode,
" RetryAfterSeconds: ", EffectiveCancelRetryCooldownSeconds());
return false;
}
//+------------------------------------------------------------------+
//| pending注文タイプをEA戦略番号へ変換する関数
//+------------------------------------------------------------------+
int PendingOrderStrategy(const ENUM_ORDER_TYPE order_type)
{
switch(order_type)
{
case ORDER_TYPE_BUY_STOP:
return 1;
case ORDER_TYPE_BUY_LIMIT:
return 2;
case ORDER_TYPE_SELL_STOP:
return 3;
case ORDER_TYPE_SELL_LIMIT:
return 4;
default:
return 0;
}
}
//+------------------------------------------------------------------+
//| H4 market_stateと矛盾するpending注文を取消する関数
//+------------------------------------------------------------------+
// Cancel pending orders that contradict the refreshed H4 market state.
bool CancelPendingOrdersNotAllowedByTrend(const int trend_state)
{
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;
int strategy = PendingOrderStrategy((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE));
if(strategy > 0 && IsOrderTypeAllowedByTrend(strategy, trend_state))
continue;
string reason = "H4 market_state=" + IntegerToString(trend_state) +
" strategy=" + IntegerToString(strategy);
if(TryCancelPendingOrder(ticket, reason))
result = true;
}
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候補時刻を復元する関数
//+------------------------------------------------------------------+
datetime PendingOrderCandidateTime(const string comment)
{
int marker = StringFind(comment, "Z");
if(marker < 0)
return 0;
datetime candidate_at = 0;
string candidate_id = StringSubstr(comment, marker + 1, 12);
if(!TryParseTargetCandidateTime(candidate_id, candidate_at))
return 0;
return candidate_at;
}
//+------------------------------------------------------------------+
//| 分割slotが既存注文/ポジションに存在するか判定する関数
//+------------------------------------------------------------------+
bool HasExistingSplitSlot(const string slot_key)
{
if(slot_key == "")
return false;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0)
continue;
if(!OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
if(StringFind(OrderGetString(ORDER_COMMENT), slot_key) >= 0)
return true;
}
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string symbol = PositionGetSymbol(i);
if(symbol == "")
continue;
if(symbol != _Symbol)
continue;
if((int)PositionGetInteger(POSITION_MAGIC) != magic_number)
continue;
if(StringFind(PositionGetString(POSITION_COMMENT), slot_key) >= 0)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 古い分割pending注文を取消する関数
//+------------------------------------------------------------------+
bool CancelStaleSplitPendingOrders(const string current_candidate_id)
{
bool result = false;
bool keep_current_candidate = (current_candidate_id != "" && current_candidate_id != "0");
CleanupPendingOrderCancelAttempts();
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0)
continue;
if(!OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
string comment = OrderGetString(ORDER_COMMENT);
if(!IsSplitOrderComment(comment))
continue;
if(keep_current_candidate && IsCurrentSplitCandidateComment(comment, current_candidate_id))
continue;
if(TryCancelPendingOrder(ticket, "stale split comment=" + comment))
result = true;
}
return result;
}
//+------------------------------------------------------------------+
//| 時間が経過したエントリー注文をキャンセルする関数
//+------------------------------------------------------------------+
/**
* @brief 実時間でENTRY_H1_LIMIT時間を超えた未約定注文をキャンセルします。
*
* @return 1件以上キャンセルに成功した場合はtrue。
*
* 対象は`_Symbol` と `magic_number` が一致する未約定注文のみです。
*/
bool CancelExpiredOrders()
{
bool result = false;
CleanupPendingOrderCancelAttempts();
// 未決済注文の数を取得
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(OrderSelect(ticket)) // 注文を選択
{
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if(OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
datetime open_time = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
datetime reference_time = PendingOrderCandidateTime(OrderGetString(ORDER_COMMENT));
if(reference_time <= 0)
reference_time = open_time;
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
if(reference_time <= 0 || expiration_seconds <= 0)
continue;
if(TimeCurrent() - reference_time >= expiration_seconds)
{
if(TryCancelPendingOrder(ticket, "H1 candidate expired"))
result = true;
}
}
}
return result;
}
#endif