2026-05-27 23:01:28 +09:00
|
|
|
|
#ifndef HIT_TRADE_MANAGER_MQH
|
|
|
|
|
|
#define HIT_TRADE_MANAGER_MQH
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-30 08:59:40 +09:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 注文ロットがブローカー制約を満たすか判定する関数
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 23:01:28 +09:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| エントリー注文を送信する関数 (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 注文ロット。
|
2026-05-31 17:17:54 +09:00
|
|
|
|
* @param comment_suffix H1候補または分割注文識別用のコメント接尾辞。空なら従来コメント。
|
|
|
|
|
|
* @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。
|
2026-05-27 23:01:28 +09:00
|
|
|
|
* @return 注文が正常に受理された場合はtrue、それ以外はfalse。
|
|
|
|
|
|
*/
|
2026-05-31 17:17:54 +09:00
|
|
|
|
bool SendOrder(int orderType, double price, double tp, double sl, double volume,
|
|
|
|
|
|
string comment_suffix, datetime candidate_reference_time)
|
2026-05-27 23:01:28 +09:00
|
|
|
|
{
|
|
|
|
|
|
MqlTradeRequest request = {};
|
|
|
|
|
|
MqlTradeResult result = {};
|
|
|
|
|
|
|
|
|
|
|
|
request.magic = magic_number;
|
|
|
|
|
|
request.symbol = _Symbol;
|
2026-05-30 08:59:40 +09:00
|
|
|
|
request.volume = NormalizeDouble(volume, VolumeDigits());
|
2026-05-27 23:01:28 +09:00
|
|
|
|
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;
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if(!ApplyPendingOrderExpiration(request, candidate_reference_time))
|
|
|
|
|
|
return false;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-30 08:59:40 +09:00
|
|
|
|
string volume_context = orderTypeStr;
|
|
|
|
|
|
if(comment_suffix != "")
|
|
|
|
|
|
volume_context += " " + comment_suffix;
|
|
|
|
|
|
if(!IsOrderVolumeAllowed(volume, volume_context))
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
2026-05-27 23:01:28 +09:00
|
|
|
|
// 注文送信
|
|
|
|
|
|
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 更新対象の取引リクエスト。
|
2026-05-31 17:17:54 +09:00
|
|
|
|
* @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。
|
|
|
|
|
|
* @return H1候補の期限が残っている場合はtrue。発注前に期限切れならfalse。
|
2026-05-27 23:01:28 +09:00
|
|
|
|
*/
|
2026-05-31 17:17:54 +09:00
|
|
|
|
bool ApplyPendingOrderExpiration(MqlTradeRequest &request, const datetime candidate_reference_time)
|
2026-05-27 23:01:28 +09:00
|
|
|
|
{
|
|
|
|
|
|
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
|
2026-05-31 17:17:54 +09:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 23:01:28 +09:00
|
|
|
|
long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE);
|
|
|
|
|
|
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if((expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0)
|
2026-05-27 23:01:28 +09:00
|
|
|
|
{
|
|
|
|
|
|
request.type_time = ORDER_TIME_SPECIFIED;
|
2026-05-31 17:17:54 +09:00
|
|
|
|
request.expiration = expiration_at;
|
|
|
|
|
|
return true;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if((expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0)
|
2026-05-27 23:01:28 +09:00
|
|
|
|
{
|
|
|
|
|
|
request.type_time = ORDER_TIME_SPECIFIED_DAY;
|
2026-05-31 17:17:54 +09:00
|
|
|
|
request.expiration = expiration_at;
|
|
|
|
|
|
return true;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if((expiration_mode & SYMBOL_EXPIRATION_DAY) != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
request.type_time = ORDER_TIME_DAY;
|
2026-05-31 17:17:54 +09:00
|
|
|
|
return true;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
request.type_time = ORDER_TIME_GTC;
|
2026-05-31 17:17:54 +09:00
|
|
|
|
return true;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 注文コメント(ローカル時刻)を生成する関数(先頭に 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 17:17:54 +09:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 11:27:10 +09:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| レンジブレイクガード用の入力値を安全な範囲へ丸める関数群
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 17:17:54 +09:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 23:01:28 +09:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 分割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;
|
2026-05-30 00:56:18 +09:00
|
|
|
|
bool keep_current_candidate = (current_candidate_id != "" && current_candidate_id != "0");
|
2026-05-31 17:17:54 +09:00
|
|
|
|
CleanupPendingOrderCancelAttempts();
|
2026-05-27 23:01:28 +09:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-30 00:56:18 +09:00
|
|
|
|
if(keep_current_candidate && IsCurrentSplitCandidateComment(comment, current_candidate_id))
|
2026-05-27 23:01:28 +09:00
|
|
|
|
continue;
|
|
|
|
|
|
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if(TryCancelPendingOrder(ticket, "stale split comment=" + comment))
|
2026-05-27 23:01:28 +09:00
|
|
|
|
result = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| 時間が経過したエントリー注文をキャンセルする関数
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 実時間でENTRY_H1_LIMIT時間を超えた未約定注文をキャンセルします。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return 1件以上キャンセルに成功した場合はtrue。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 対象は`_Symbol` と `magic_number` が一致する未約定注文のみです。
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool CancelExpiredOrders()
|
|
|
|
|
|
{
|
|
|
|
|
|
bool result = false;
|
2026-05-31 17:17:54 +09:00
|
|
|
|
CleanupPendingOrderCancelAttempts();
|
2026-05-27 23:01:28 +09:00
|
|
|
|
|
|
|
|
|
|
// 未決済注文の数を取得
|
|
|
|
|
|
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);
|
2026-05-31 17:17:54 +09:00
|
|
|
|
datetime reference_time = PendingOrderCandidateTime(OrderGetString(ORDER_COMMENT));
|
|
|
|
|
|
if(reference_time <= 0)
|
|
|
|
|
|
reference_time = open_time;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
|
|
|
|
|
|
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if(reference_time <= 0 || expiration_seconds <= 0)
|
2026-05-27 23:01:28 +09:00
|
|
|
|
continue;
|
|
|
|
|
|
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if(TimeCurrent() - reference_time >= expiration_seconds)
|
2026-05-27 23:01:28 +09:00
|
|
|
|
{
|
2026-05-31 17:17:54 +09:00
|
|
|
|
if(TryCancelPendingOrder(ticket, "H1 candidate expired"))
|
|
|
|
|
|
result = true;
|
2026-05-27 23:01:28 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
#endif
|