#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_state(0..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