#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; } //+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+ //| 時間が経過したポジションをクローズする関数 //+------------------------------------------------------------------+ /** * @brief 実時間でCLOSE_H1_LIMIT時間を超えた保有ポジションをクローズします。 * * @return 1件以上クローズに成功した場合はtrue。 * * 対象は`_Symbol` と `magic_number` が一致するポジションのみです。 */ bool CloseExpiredPositions() { bool result = false; // ポジションを逆順でループ for(int i = PositionsTotal() - 1; i >= 0; i--) { string position_symbol = PositionGetSymbol(i); if(position_symbol!="") { // Magic Number とシンボルでフィルタリング if(position_symbol != _Symbol) continue; if(PositionGetInteger(POSITION_MAGIC) == magic_number) { // ポジションのエントリー時刻を取得 datetime entry_time = (datetime)PositionGetInteger(POSITION_TIME); int expiration_seconds = CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1); if(entry_time <= 0 || expiration_seconds <= 0) continue; if(TimeCurrent() - entry_time >= expiration_seconds) { MqlTradeRequest request = {}; MqlTradeResult trade_result = {}; // ポジションタイプに応じてリクエストを設定 int position_type = (int)PositionGetInteger(POSITION_TYPE); request.action = TRADE_ACTION_DEAL; request.position = PositionGetInteger(POSITION_TICKET); // ポジションのチケット番号 request.symbol = position_symbol; // シンボル request.volume = PositionGetDouble(POSITION_VOLUME); // ポジションサイズ request.price = (position_type == POSITION_TYPE_BUY) ? SymbolInfoDouble(position_symbol, SYMBOL_BID) // BUYの場合はBIDでクローズ : SymbolInfoDouble(position_symbol, SYMBOL_ASK); // SELLの場合はASKでクローズ request.deviation = slippage; request.type = (position_type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL // BUYポジションをSELLでクローズ : ORDER_TYPE_BUY; // SELLポジションをBUYでクローズ request.type_filling = GetOrderFillingPolicy(position_symbol); // 注文執行ポリシー request.magic = magic_number; // 注文送信 if(!OrderSend(request, trade_result)) { Print("Failed to close position. Ticket: ", request.position, " Error: ", GetLastError()); continue; } // 結果の確認 if(trade_result.retcode == TRADE_RETCODE_DONE || trade_result.retcode == TRADE_RETCODE_DONE_PARTIAL) { Print("Position closed successfully due to a time limit. Ticket: ", request.position); result = true; // 少なくとも1つ成功した場合 } else { Print("Failed to close position. Ticket: ", request.position, " Retcode: ", trade_result.retcode); } } } } else { Print("Failed to select position at index ", i, ". Error: ", GetLastError()); } } return result; } #endif