From 33cfc2597ce64d1233f60a5023edaa578bc83ec1 Mon Sep 17 00:00:00 2001 From: Hiroaki86 Date: Sun, 31 May 2026 17:17:54 +0900 Subject: [PATCH] Harden pending cancellation and startup state handling --- Experts/MyProject/HIT-EA_refactor_ver6.mq5 | 27 +- ...efactor_ver6.mq5.bak_codex_20260531_170419 | 609 ++++++++++ Include/MyLib/Common/HITRuntimeController.mqh | 2 + ...meController.mqh.bak_codex_20260531_170419 | 289 +++++ Include/MyLib/Signals/HITEntrySignal.mqh | 28 +- ...TEntrySignal.mqh.bak_codex_20260531_170419 | 1070 +++++++++++++++++ .../MyLib/Signals/HITPythonSignalGateway.mqh | 41 +- ...ignalGateway.mqh.bak_codex_20260531_170419 | 725 +++++++++++ Include/MyLib/Trading/HITTradeManager.mqh | 306 ++++- ...TradeManager.mqh.bak_codex_20260531_170419 | 625 ++++++++++ docs/MyProject/HIT-EA_spec_en.md | 27 +- ...IT-EA_spec_en.md.bak_codex_20260531_170419 | 259 ++++ docs/MyProject/HIT-EA_spec_ja.md | 27 +- ...IT-EA_spec_ja.md.bak_codex_20260531_170419 | 259 ++++ 14 files changed, 4196 insertions(+), 98 deletions(-) create mode 100644 Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260531_170419 create mode 100644 Include/MyLib/Common/HITRuntimeController.mqh.bak_codex_20260531_170419 create mode 100644 Include/MyLib/Signals/HITEntrySignal.mqh.bak_codex_20260531_170419 create mode 100644 Include/MyLib/Signals/HITPythonSignalGateway.mqh.bak_codex_20260531_170419 create mode 100644 Include/MyLib/Trading/HITTradeManager.mqh.bak_codex_20260531_170419 create mode 100644 docs/MyProject/HIT-EA_spec_en.md.bak_codex_20260531_170419 create mode 100644 docs/MyProject/HIT-EA_spec_ja.md.bak_codex_20260531_170419 diff --git a/Experts/MyProject/HIT-EA_refactor_ver6.mq5 b/Experts/MyProject/HIT-EA_refactor_ver6.mq5 index d8c87f0..3528d32 100644 --- a/Experts/MyProject/HIT-EA_refactor_ver6.mq5 +++ b/Experts/MyProject/HIT-EA_refactor_ver6.mq5 @@ -100,7 +100,7 @@ enum SplitLotMode input double lot_size = 0.01; // ロット数 input double spread_limit = 60; // 許容スプレッド(point) input int magic_number = 10001; // マジックナンバー -input int initial_order = 0; // 起動時の注文(0:なし, 1:あり) +input int initial_order = 0; // 互換性維持用。起動時H4/H1再構築は常時実行 input int input_position_limit = 10; // 自EAの未約定注文+ポジション上限 input bool use_split_entry_zone = false; // H1予測ゾーンで分割エントリーする input int split_entry_count = 3; // 分割エントリー本数(1..10) @@ -108,6 +108,7 @@ input SplitLotMode split_lot_mode = SPLIT_LOT_TOTAL; // 分割ロット配分モ input double split_total_lot_size = 0.09; // 総量分割モードの合計ロット input double split_fixed_lot_size = 0.01; // 固定ロットモードの1注文ロット input bool cancel_old_split_pending_on_new_zone = true; // 新ゾーン読込時に旧分割pendingを取消 +input int input_cancel_retry_cooldown_seconds = 60; // pending取消失敗時のticket単位再試行間隔 input bool use_m15_entry_filter = true; // M15確定足で発注タイミングを確認 input double m15_entry_zone_atr_multiplier = 1.50; // M15平均レンジ何本分まで候補価格への接近を許可 input bool use_m15_imbalance_confirmation = true; // T1/T3でM15初動確認を追加 @@ -192,8 +193,8 @@ struct EAState EAState g_ea; // EA全体の状態をここで管理 -// initial_order 用の初回処理フラグ -// H4トレンド処理とH1エントリー処理で別々に管理し、初回起動時の再実行ループを防ぐ。 +// Startup refresh flags for H4 trend and H1 entry candidates. +// Keep them separate to prevent repeated startup launches. bool g_init_trend_pending = false; bool g_init_entry_pending = false; @@ -204,6 +205,10 @@ int g_pre_bars_M15 = 0; bool g_bars_H1_check = false; bool g_bars_M15_check = false; +// pending取消失敗時の過剰な取引サーバー要求を防止する。 +ulong g_cancel_retry_tickets[]; +datetime g_cancel_retry_at[]; + // ティックごとの価格情報 struct TickContext { @@ -379,9 +384,9 @@ void ConfigurePythonGatewayPaths() /** * @brief EA起動時の初期化処理を行います。 * - * Python連携用のdone/runningファイル状態を確認し、`initial_order` が有効な場合は + * Python連携用のdone/runningファイル状態を確認し、起動直後は安全側として * H4トレンド判定とH1エントリー価格生成をそれぞれ初回実行対象にします。 - * H4/H1の初回フラグを分けることで、起動直後にH4処理だけが繰り返される状態を防ぎます。 + * H4結果を再読込するまでは技術エラー停止値を維持し、古い連携結果での発注を防ぎます。 */ int OnInit() // プロセス完了ファイルと実行中ファイルの状態を初期化 @@ -391,9 +396,15 @@ int OnInit() PrepareDoneFileOnInit(done_trend_file, running_trend_file, "trend"); PrepareDoneFileOnInit(done_entry_file, running_entry_file, "entry"); - // initial_order=1 の場合でも、H4/H1を別々に1回だけ初回実行する。 - g_init_trend_pending = (initial_order == 1); - g_init_entry_pending = (initial_order == 1); + // Always rebuild startup state before allowing new orders. + g_ea.trend_state = MARKET_TECHNICAL_ERROR_STOP; + g_ea.res_chk = 0; + g_ea.zone_res_chk = 0; + g_ea.zone_candidate_id = "0"; + g_init_trend_pending = true; + g_init_entry_pending = true; + ArrayResize(g_cancel_retry_tickets, 0); + ArrayResize(g_cancel_retry_at, 0); if(!InitializeSLTPManager()) return INIT_FAILED; diff --git a/Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260531_170419 b/Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260531_170419 new file mode 100644 index 0000000..d8c87f0 --- /dev/null +++ b/Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260531_170419 @@ -0,0 +1,609 @@ +//+------------------------------------------------------------------+ +//| HIT_EA.mq5 | +//| Copyright 2026, nanpin-martin.com | +//| https://www.nanpin-martin.com | +//+------------------------------------------------------------------+ +#property copyright "Copyright 2026, nanpin-martin.com" +#property link "https://nanpin-martin.com/" +#property version "1.01" + +#include +#include + +//+------------------------------------------------------------------+ +//| バッチファイル実行用の設定 +//+------------------------------------------------------------------+ + +// Windowsプロセス起動・終了確認用 +#define CREATE_NO_WINDOW 0x08000000 +#define PROCESS_QUERY_LIMITED_INFORMATION 0x00001000 +#define SYNCHRONIZE 0x00100000 +#define WAIT_OBJECT_0 0 +#define WAIT_TIMEOUT 258 +#define STILL_ACTIVE 259 +#define TASKKILL_WAIT_MILLISECONDS 5000 + +struct STARTUPINFO_W + { + uint cb; + string lpReserved; + string lpDesktop; + string lpTitle; + uint dwX; + uint dwY; + uint dwXSize; + uint dwYSize; + uint dwXCountChars; + uint dwYCountChars; + uint dwFillAttribute; + uint dwFlags; + ushort wShowWindow; + ushort cbReserved2; + long lpReserved2; + long hStdInput; + long hStdOutput; + long hStdError; + }; + +struct PROCESS_INFORMATION + { + long hProcess; + long hThread; + uint dwProcessId; + uint dwThreadId; + }; + +#import "kernel32.dll" +int CreateProcessW(string lpApplicationName, string lpCommandLine, long lpProcessAttributes, long lpThreadAttributes, int bInheritHandles, uint dwCreationFlags, long lpEnvironment, string lpCurrentDirectory, STARTUPINFO_W &lpStartupInfo, PROCESS_INFORMATION &lpProcessInformation); +int WaitForSingleObject(long hHandle, uint dwMilliseconds); +int GetExitCodeProcess(long hProcess, uint &lpExitCode); +int CloseHandle(long hObject); +long OpenProcess(uint dwDesiredAccess, int bInheritHandle, uint dwProcessId); +#import + +// バッチファイルのパス +// Python helper scripts are managed under this MT5 data folder: +// \MQL5\python_for_ea +string python_app_dir = ""; +string get_trend_reply_bat = ""; // H4: trend +string get_entry_reply_bat = ""; // H1: entry +string mt5_file_prefix = ""; // Python linkage file namespace + +// プロセス完了ファイルの設定 +string done_trend_file = "process_done_trend.txt"; +string done_entry_file = "process_done_entry.txt"; + +// Python出力ファイル +string ohlc_h4_file = "ohlc_H4.csv"; +string ohlc_h1_file = "ohlc_H1.csv"; +string trend_state_file = "trend_state.txt"; +string target_prices_file = "target_prices.txt"; +string target_zones_file = "target_zones.txt"; + +// Python実行中を判定するための管理ファイル +string running_trend_file = "process_running_trend.txt"; +string running_entry_file = "process_running_entry.txt"; + +//+------------------------------------------------------------------+ + +//+------------------------------------------------------------------+ +//| インプットの設定 +//+------------------------------------------------------------------+ + +enum SplitLotMode + { + SPLIT_LOT_TOTAL = 0, // 総ロットをN分割 + SPLIT_LOT_FIXED = 1 // 1注文あたり固定ロット + }; + +//--- 入力パラメータ +input double lot_size = 0.01; // ロット数 +input double spread_limit = 60; // 許容スプレッド(point) +input int magic_number = 10001; // マジックナンバー +input int initial_order = 0; // 起動時の注文(0:なし, 1:あり) +input int input_position_limit = 10; // 自EAの未約定注文+ポジション上限 +input bool use_split_entry_zone = false; // H1予測ゾーンで分割エントリーする +input int split_entry_count = 3; // 分割エントリー本数(1..10) +input SplitLotMode split_lot_mode = SPLIT_LOT_TOTAL; // 分割ロット配分モード +input double split_total_lot_size = 0.09; // 総量分割モードの合計ロット +input double split_fixed_lot_size = 0.01; // 固定ロットモードの1注文ロット +input bool cancel_old_split_pending_on_new_zone = true; // 新ゾーン読込時に旧分割pendingを取消 +input bool use_m15_entry_filter = true; // M15確定足で発注タイミングを確認 +input double m15_entry_zone_atr_multiplier = 1.50; // M15平均レンジ何本分まで候補価格への接近を許可 +input bool use_m15_imbalance_confirmation = true; // T1/T3でM15初動確認を追加 +input int m15_imbalance_avg_body_period = 20; // M15平均実体計算期間 +input double m15_imbalance_sensitivity = 2.0; // M15初動検知感度 +input double m15_imbalance_min_avg_body_points = 1.0; // 平均実体の最小値(point) +input bool use_m15_imbalance_debug_log = false; // M15初動確認ログ +input int input_entry_max_candidate_age_minutes = 120; // H1候補価格を新規発注に使う最大経過分数 +input bool input_sltp_manager_enabled = false; // UI連動SLTP管理を有効化 +input bool input_sltp_show_panel = true; // SLTP操作パネルを表示 +input bool input_sltp_use_breakeven = false; // 通常ブレークイーブンを有効化 +input double input_sltp_breakeven_trigger_pips = 30.0; // 通常BE開始pips +input double input_sltp_breakeven_buffer_pips = 3.0; // 通常BE固定バッファpips +input bool input_sltp_use_elapsed_breakeven = false; // 保有時間BEを有効化 +input double input_sltp_elapsed_breakeven_hours = 4.0; // 保有時間BE開始時間 +input double input_sltp_elapsed_breakeven_buffer_pips = 3.0; // 保有時間BE固定バッファpips +input bool input_sltp_use_active_trailing = false; // アクティブトレーリングを有効化 +input double input_sltp_active_breakeven_pips = 30.0; // アクティブ開始pips +input double input_sltp_active_stop_loss_offset_pips = 5.0; // 初期固定pips +input double input_sltp_active_step_trigger_pips = 10.0; // ステップ更新間隔pips +input double input_sltp_active_step_move_pips = 5.0; // 1ステップ固定追加pips +input bool input_sltp_use_tp_progress_stop = false; // TP進捗SLを有効化 +input double input_sltp_tp_progress_trigger_percent = 70.0; // TP進捗SL開始率 +input double input_sltp_tp_progress_sl_lock_percent = 30.0; // TP距離固定率 +input bool input_sltp_use_high_volatility_limit = false; // 急変時SL引き締めを有効化 +ulong slippage = 10; // スリッページ + +//--- 主要な定数・設定 +#define POSITION_LIMIT 48 +#define ENTRY_H1_LIMIT 2 // H1本数(=2時間) +#define CLOSE_H1_LIMIT 12 // H1本数(=12時間) +#define HISTORY_BARS 72 +#define OHLC_START_SHIFT 1 // 1: 確定足のみをPythonへ渡す +#define M15_CONFIRM_BARS 30 +#define M15_MIN_ENTRY_ZONE_POINTS 10 +#define M15_MIN_BODY_RATIO 0.25 +#define M15_REJECTION_WICK_RATIO 0.35 +#define ENTRY_RETRY_SECONDS 60 +#define ENTRY_RETRY_LIMIT 10 +// ANALYZE_TIMEFRAME は削除(H4/H1 両方使うため定数ではなく直接指定) +#define TARGET_SIZE 13 +#define TARGET_ZONE_SCHEMA_VERSION 2 +#define DEFAULT_TARGET_PRICE 0.0 +#define PYTHON_TIMEOUT_SECONDS 600 // Python完了待ちの上限(秒) + +#define MARKET_LOW_VOL_RANGE 0 +#define MARKET_HIGH_VOL_RANGE 1 +#define MARKET_LOW_VOL_UP 2 +#define MARKET_HIGH_VOL_UP 3 +#define MARKET_LOW_VOL_DOWN 4 +#define MARKET_HIGH_VOL_DOWN 5 +#define MARKET_TECHNICAL_ERROR_STOP 6 + +//+------------------------------------------------------------------+ + +//+------------------------------------------------------------------+ +//| EAの状態をまとめる構造体 +//+------------------------------------------------------------------+ + +struct EAState + { + int trend_state; // H4 market_state(0..5、6は技術エラー停止)を格納する変数 + int res_chk; // GPTの回答が正しく取得できたか + double en_price[5]; // エントリー基準価格 + double tp_price[5]; // 利益確定価格 + double sl_price[5]; // ロスカット基準価格 + int zone_res_chk; // 予測ゾーンが正しく取得できたか + string zone_candidate_id; // H1確定足由来の候補ID + double zone_low[5]; // 分割エントリー用ゾーン下限 + double zone_high[5]; // 分割エントリー用ゾーン上限 + double zone_tp[5]; // 分割エントリー用TP + double zone_sl[5]; // 分割エントリー用SL + bool load_trend_flg; // ★変更:トレンドを更新するタイミングか + bool load_target_flg; // ★変更:ターゲット価格を更新するタイミングか + int chk_cnt; // エントリー判定の試行回数 + datetime last_trend_update; // ★追加:前回トレンドを更新した時刻 + datetime last_target_update; // ★変更:前回ターゲット価格を更新した時刻 + datetime target_loaded_at; // H1候補価格をEAへ読み込んだサーバー時刻 + datetime target_candidate_at; // H1候補価格の元になった確定足時刻 + datetime last_chk; + }; + +EAState g_ea; // EA全体の状態をここで管理 + +// initial_order 用の初回処理フラグ +// H4トレンド処理とH1エントリー処理で別々に管理し、初回起動時の再実行ループを防ぐ。 +bool g_init_trend_pending = false; +bool g_init_entry_pending = false; + +// H4/H1のバー更新状態 +int g_pre_bars_H4 = 0; +int g_pre_bars_H1 = 0; +int g_pre_bars_M15 = 0; +bool g_bars_H1_check = false; +bool g_bars_M15_check = false; + +// ティックごとの価格情報 +struct TickContext + { + double ask; + double bid; + double spread; + double spread_points; + int digits; + }; + +// 外部Pythonプロセスの状態 +struct ExternalProcessState + { + long handle; + uint process_id; + bool active; + bool exit_code_ready; + uint exit_code; + datetime started_at; + }; + +ExternalProcessState g_trend_process; +ExternalProcessState g_entry_process; + +CSLTPManager g_sltp_manager; +CSLTPManagerPanel g_sltp_panel; +SLTPManagerPanelSettings g_sltp_settings; +bool g_sltp_settings_valid = false; +bool g_sltp_panel_created = false; + +//+------------------------------------------------------------------+ + +//+------------------------------------------------------------------+ +//| Python連携パスをMT5データフォルダから組み立てる関数 +//+------------------------------------------------------------------+ +/** + * @brief MT5データフォルダ配下のMQL5ルートを返します。 + */ +string MQL5RootPath() + { + return TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5"; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief EAと同じMQL5ツリー内にあるPython補助アプリのルートを返します。 + */ +string PythonAppDir() + { + return MQL5RootPath() + "\\python_for_ea"; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief Python補助アプリ配下のbatファイル絶対パスを返します。 + */ +string PythonBatchPath(const string filename) + { + return PythonAppDir() + "\\bat\\" + filename; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief ファイル名に使える安全なトークン文字か判定します。 + */ +bool IsSafeFileTokenChar(const int ch) + { + return ((ch >= 48 && ch <= 57) || // 0-9 + (ch >= 65 && ch <= 90) || // A-Z + (ch >= 97 && ch <= 122) || // a-z + ch == 95 || ch == 45); // _ or - + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief シンボル名などをファイル名向けの安全なトークンへ変換します。 + */ +string SanitizeFileToken(const string value) + { + string result = ""; + int length = StringLen(value); + for(int i = 0; i < length; i++) + { + int ch = StringGetCharacter(value, i); + if(IsSafeFileTokenChar(ch)) + result += StringSubstr(value, i, 1); + else + result += "_"; + } + + if(result == "") + return "UNKNOWN"; + + return result; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief 同一Terminal内の複数EA/複数シンボルで連携ファイルが衝突しない接頭辞を返します。 + */ +string BuildMT5FilePrefix() + { + return "HIT_" + SanitizeFileToken(_Symbol) + "_" + IntegerToString(magic_number); + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief Python連携ファイル名へEA固有の接頭辞を付けます。 + */ +string PrefixedMT5FileName(const string base_name) + { + if(mt5_file_prefix == "") + return base_name; + + return mt5_file_prefix + "_" + base_name; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief Python連携で使用するMT5 Files配下のファイル名を初期化します。 + */ +void ConfigurePythonGatewayFileNames() + { + mt5_file_prefix = BuildMT5FilePrefix(); + + ohlc_h4_file = PrefixedMT5FileName("ohlc_H4.csv"); + ohlc_h1_file = PrefixedMT5FileName("ohlc_H1.csv"); + done_trend_file = PrefixedMT5FileName("process_done_trend.txt"); + done_entry_file = PrefixedMT5FileName("process_done_entry.txt"); + trend_state_file = PrefixedMT5FileName("trend_state.txt"); + target_prices_file = PrefixedMT5FileName("target_prices.txt"); + target_zones_file = PrefixedMT5FileName("target_zones.txt"); + running_trend_file = PrefixedMT5FileName("process_running_trend.txt"); + running_entry_file = PrefixedMT5FileName("process_running_entry.txt"); + + Print("Python file prefix: ", mt5_file_prefix); + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief Python連携で使用するbatパスを初期化します。 + */ +void ConfigurePythonGatewayPaths() + { + ConfigurePythonGatewayFileNames(); + + python_app_dir = PythonAppDir(); + get_trend_reply_bat = PythonBatchPath("get_trend_reply.bat"); + get_entry_reply_bat = PythonBatchPath("get_entry_reply.bat"); + + Print("Python app dir: ", python_app_dir); + } + +//+------------------------------------------------------------------+ +//| 起動時の処理 +//+------------------------------------------------------------------+ +/** + * @brief EA起動時の初期化処理を行います。 + * + * Python連携用のdone/runningファイル状態を確認し、`initial_order` が有効な場合は + * H4トレンド判定とH1エントリー価格生成をそれぞれ初回実行対象にします。 + * H4/H1の初回フラグを分けることで、起動直後にH4処理だけが繰り返される状態を防ぎます。 + */ +int OnInit() +// プロセス完了ファイルと実行中ファイルの状態を初期化 + { + ConfigurePythonGatewayPaths(); + + PrepareDoneFileOnInit(done_trend_file, running_trend_file, "trend"); + PrepareDoneFileOnInit(done_entry_file, running_entry_file, "entry"); + + // initial_order=1 の場合でも、H4/H1を別々に1回だけ初回実行する。 + g_init_trend_pending = (initial_order == 1); + g_init_entry_pending = (initial_order == 1); + + if(!InitializeSLTPManager()) + return INIT_FAILED; + + return INIT_SUCCEEDED; + } + +//+------------------------------------------------------------------+ +//| 終了時の処理 +//+------------------------------------------------------------------+ +void OnDeinit(const int reason) + { + ResetExternalProcessState(g_trend_process); + ResetExternalProcessState(g_entry_process); + + if(g_sltp_panel_created) + g_sltp_panel.Destroy(reason); + + // Clear chart status text left by Comment() when the EA is removed. + Comment(""); + ChartRedraw(0); + } + +//+------------------------------------------------------------------+ +//| ティック毎の処理 +//+------------------------------------------------------------------+ +/** + * @brief ティック受信ごとにEA全体の処理を制御します。 + * + * 処理順序は、ティック情報取得、期限切れ注文/ポジション管理、H4トレンド更新、 + * H1エントリー価格更新、M15エントリータイミング更新、ステータス表示、新規注文判定の順です。 + * 時間制限処理はPython完了待ちやスプレッド判定より前に実行します。 + */ +void OnTick() + { + TickContext ctx; + if(!GetTickContext(ctx)) + return; + + // 期限切れ注文・ポジションは、Python待ちやスプレッド制限に関係なく先に処理する。 + ManageExpiredTrades(); + + // Existing-position protection stays active even while Python results are pending. + ManageSLTPPositions(); + + // 外部Pythonがdoneを返さない場合に、一定時間で待機状態を解除して再実行対象にする。 + RecoverTimedOutPythonProcesses(); + + // H4新バーまたは初回起動時に、トレンド判定用Pythonを起動する。 + ProcessTrendUpdate(g_ea); + + // トレンド判定が完了していない場合、新規注文側の処理だけをスキップする。 + if(!IsTrendResultReady()) + return; + + RefreshTrendState(g_ea); + + // H1新バーまたは初回起動時に、エントリー価格生成用Pythonを起動する。 + ProcessEntryUpdate(g_ea); + + // M15確定足ごとに、H1候補価格を発注してよいタイミングか再判定する。 + ProcessM15EntryTimingUpdate(); + + UpdateStatusComment(ctx); + + // ここから下は新規注文に関する処理。 + if(!IsSpreadAllowed(ctx)) + return; + + if(!IsEntryResultReady()) + return; + + RefreshTargetPrices(g_ea); + ProcessEntryDecisionIfNeeded(g_ea, ctx); + } + +//+------------------------------------------------------------------+ +//| SLTP manager panel and settings bridge +//+------------------------------------------------------------------+ +string SLTPBoolText(const bool value) + { + return value ? "ON" : "OFF"; + } + +void LoadSLTPInputSettings(SLTPManagerPanelSettings &settings) + { + settings.manager_enabled = input_sltp_manager_enabled; + + settings.use_breakeven = input_sltp_use_breakeven; + settings.breakeven_trigger_pips = input_sltp_breakeven_trigger_pips; + settings.breakeven_buffer_pips = input_sltp_breakeven_buffer_pips; + + settings.use_elapsed_breakeven = input_sltp_use_elapsed_breakeven; + settings.elapsed_breakeven_hours = input_sltp_elapsed_breakeven_hours; + settings.elapsed_breakeven_buffer_pips = input_sltp_elapsed_breakeven_buffer_pips; + + settings.use_active_trailing = input_sltp_use_active_trailing; + settings.active_breakeven_pips = input_sltp_active_breakeven_pips; + settings.active_stop_loss_offset_pips = input_sltp_active_stop_loss_offset_pips; + settings.active_step_trigger_pips = input_sltp_active_step_trigger_pips; + settings.active_step_move_pips = input_sltp_active_step_move_pips; + + settings.use_tp_progress_stop = input_sltp_use_tp_progress_stop; + settings.tp_progress_trigger_percent = input_sltp_tp_progress_trigger_percent; + settings.tp_progress_sl_lock_percent = input_sltp_tp_progress_sl_lock_percent; + + settings.use_high_volatility_limit = input_sltp_use_high_volatility_limit; + } + +bool ApplySLTPSettings(const SLTPManagerPanelSettings &settings, + const bool print_summary) + { + g_sltp_settings_valid = false; + + g_sltp_manager.SetMagicNumber((ulong)magic_number); + g_sltp_manager.SetSymbol(_Symbol); + g_sltp_manager.SetDeviationInPoints(slippage); + g_sltp_manager.SetBreakevenSettings(settings.use_breakeven, + settings.breakeven_trigger_pips, + settings.breakeven_buffer_pips); + g_sltp_manager.SetElapsedBreakevenSettings(settings.use_elapsed_breakeven, + settings.elapsed_breakeven_hours, + settings.elapsed_breakeven_buffer_pips); + g_sltp_manager.SetActiveTrailingSettings(settings.use_active_trailing, + settings.active_breakeven_pips, + settings.active_stop_loss_offset_pips, + settings.active_step_trigger_pips, + settings.active_step_move_pips); + g_sltp_manager.SetTpProgressStopSettings(settings.use_tp_progress_stop, + settings.tp_progress_trigger_percent, + settings.tp_progress_sl_lock_percent); + g_sltp_manager.SetHighVolatilityLimitSettings(settings.use_high_volatility_limit); + + if(!g_sltp_manager.ValidateSettings()) + { + Print("HIT SLTP settings rejected. Manager is disabled until valid settings are applied."); + return false; + } + + g_sltp_settings = settings; + g_sltp_settings_valid = true; + + if(print_summary) + { + Print("HIT SLTP settings: Manager=", SLTPBoolText(settings.manager_enabled), + " BE=", SLTPBoolText(settings.use_breakeven), + " ElapsedBE=", SLTPBoolText(settings.use_elapsed_breakeven), + " ActiveTrail=", SLTPBoolText(settings.use_active_trailing), + " TPProgress=", SLTPBoolText(settings.use_tp_progress_stop), + " HighVol=", SLTPBoolText(settings.use_high_volatility_limit)); + } + + return true; + } + +bool InitializeSLTPManager() + { + SLTPManagerPanelSettings initial_settings; + LoadSLTPInputSettings(initial_settings); + + if(!ApplySLTPSettings(initial_settings, true)) + return false; + + g_sltp_panel.Init(g_sltp_settings); + if(input_sltp_show_panel) + { + if(!g_sltp_panel.CreatePanel()) + { + Print("HIT SLTP panel is unavailable. EA continues with input-based SLTP settings."); + return true; + } + + g_sltp_panel.SetInitialValues(); + g_sltp_panel_created = true; + } + + return true; + } + +void ManageSLTPPositions() + { + if(!g_sltp_settings_valid) + return; + if(!g_sltp_settings.manager_enabled) + return; + + g_sltp_manager.ManagePositions(); + g_sltp_manager.HighVolatilityLimit(); + } + +void OnChartEvent(const int id, + const long &lparam, + const double &dparam, + const string &sparam) + { + if(!g_sltp_panel_created) + return; + + SLTPManagerPanelSettings candidate_settings = g_sltp_settings; + if(g_sltp_panel.HandleChartEvent(id, lparam, dparam, sparam, candidate_settings)) + { + if(!ApplySLTPSettings(candidate_settings, true)) + Print("HIT SLTP panel apply failed. Please review panel values."); + } + } + + +// Function implementations are kept in project headers so the EA entry points stay compact. +#include +#include +#include +#include +#include diff --git a/Include/MyLib/Common/HITRuntimeController.mqh b/Include/MyLib/Common/HITRuntimeController.mqh index ce67172..cd5b3d0 100644 --- a/Include/MyLib/Common/HITRuntimeController.mqh +++ b/Include/MyLib/Common/HITRuntimeController.mqh @@ -36,6 +36,8 @@ bool GetTickContext(TickContext &ctx) */ void ManageExpiredTrades() { + CleanupPendingOrderCancelAttempts(); + if(OrdersTotal() > 0) CancelExpiredOrders(); diff --git a/Include/MyLib/Common/HITRuntimeController.mqh.bak_codex_20260531_170419 b/Include/MyLib/Common/HITRuntimeController.mqh.bak_codex_20260531_170419 new file mode 100644 index 0000000..ce67172 --- /dev/null +++ b/Include/MyLib/Common/HITRuntimeController.mqh.bak_codex_20260531_170419 @@ -0,0 +1,289 @@ +#ifndef HIT_RUNTIME_CONTROLLER_MQH +#define HIT_RUNTIME_CONTROLLER_MQH + +//+------------------------------------------------------------------+ +//| ティック情報を取得する関数 +//+------------------------------------------------------------------+ +/** + * @brief 現在のAsk/Bid/スプレッド情報を取得してTickContextへ格納します。 + * + * @param ctx 取得したティック情報を格納する構造体参照。 + * @return 取得成功時はtrue、ティック情報を取得できない場合はfalse。 + */ +bool GetTickContext(TickContext &ctx) + { + MqlTick last_tick; + if(!SymbolInfoTick(_Symbol, last_tick)) + return false; + + ctx.ask = last_tick.ask; + ctx.bid = last_tick.bid; + ctx.spread_points = MathRound((ctx.ask - ctx.bid) / Point()); + ctx.spread = ctx.spread_points * Point(); + ctx.digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + + return true; + } + +//+------------------------------------------------------------------+ +//| 期限切れ注文・期限切れポジションを処理する関数 +//+------------------------------------------------------------------+ +/** + * @brief 期限切れの未約定注文と保有ポジションを処理します。 + * + * 未約定注文はENTRY_H1_LIMIT時間、保有ポジションはCLOSE_H1_LIMIT時間を基準に判定します。 + * 新規注文条件とは独立して、OnTickの早い段階で実行される想定です。 + */ +void ManageExpiredTrades() + { + if(OrdersTotal() > 0) + CancelExpiredOrders(); + + if(PositionsTotal() > 0) + CloseExpiredPositions(); + } + +//+------------------------------------------------------------------+ +//| H4更新検知とトレンド判定Python起動を処理する関数 +//+------------------------------------------------------------------+ +/** + * @brief H4新バーまたは初回起動を検知し、トレンド判定Pythonを起動します。 + * + * @param state EA全体の状態。トレンド更新開始フラグを更新します。 + * + * Python処理中はdoneファイルが存在しないため、CSVの上書きと二重起動を抑止します。 + */ +void ProcessTrendUpdate(EAState &state) + { + int current_bars_H4 = iBars(NULL, PERIOD_H4); + if(g_pre_bars_H4 == 0) + g_pre_bars_H4 = current_bars_H4; + + int bars_H4_change = current_bars_H4 - g_pre_bars_H4; + bool trend_trigger = (bars_H4_change > 0 || g_init_trend_pending); + + if(!trend_trigger) + return; + + if(RecordOHLCAndExecuteBatch_Trend(state)) + { + g_init_trend_pending = false; + g_pre_bars_H4 = current_bars_H4; + } + } + +//+------------------------------------------------------------------+ +//| トレンド判定Pythonの完了状態を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief H4トレンド判定Pythonの完了状態を確認します。 + * + * @return `process_done_trend.txt` が存在する場合はtrue、未完了の場合はfalse。 + */ +bool IsTrendResultReady() + { + return IsProcessResultReady(done_trend_file, running_trend_file, trend_state_file, "trend", g_trend_process); + } + +//+------------------------------------------------------------------+ +//| トレンド判定結果をEA状態へ反映する関数 +//+------------------------------------------------------------------+ +/** + * @brief 完了済みのH4トレンド判定結果をEA状態へ反映します。 + * + * @param state EA全体の状態。`trend_state` と最終更新時刻が更新されます。 + */ +void RefreshTrendState(EAState &state) + { + GetTrendState(state); + } + +//+------------------------------------------------------------------+ +//| H1更新検知とエントリー価格生成Python起動を処理する関数 +//+------------------------------------------------------------------+ +/** + * @brief H1新バーまたは初回起動を検知し、エントリー価格生成Pythonを起動します。 + * + * @param state EA全体の状態。ターゲット価格更新開始フラグや判定リトライ状態を更新します。 + * + * Python処理中はdoneファイルが存在しないため、H1 CSVの上書きと二重起動を抑止します。 + */ +void ProcessEntryUpdate(EAState &state) + { + int current_bars_H1 = iBars(NULL, PERIOD_H1); + if(g_pre_bars_H1 == 0) + g_pre_bars_H1 = current_bars_H1; + + int bars_H1_change = current_bars_H1 - g_pre_bars_H1; + bool entry_trigger = (bars_H1_change > 0 || g_init_entry_pending); + + if(!entry_trigger) + return; + + if(RecordOHLCAndExecuteBatch_Entry(state)) + { + g_init_entry_pending = false; + g_bars_H1_check = true; + state.chk_cnt = 0; + state.last_chk = 0; + g_pre_bars_H1 = current_bars_H1; + } + } + +//+------------------------------------------------------------------+ +//| M15更新検知とエントリータイミング判定トリガーを処理する関数 +//+------------------------------------------------------------------+ +/** + * @brief M15新バーを検知し、H1候補価格の発注判定を実行できる状態にします。 + * + * M15は方向判定を上書きする足ではなく、H4/H1で決めた候補価格を発注する + * タイミング確認として使います。初回は直近の確定M15足で一度だけ判定可能にします。 + */ +void ProcessM15EntryTimingUpdate() + { + int current_bars_M15 = iBars(NULL, PERIOD_M15); + if(current_bars_M15 <= 0) + return; + + if(g_pre_bars_M15 == 0) + { + g_pre_bars_M15 = current_bars_M15; + g_bars_M15_check = true; + return; + } + + int bars_M15_change = current_bars_M15 - g_pre_bars_M15; + if(bars_M15_change <= 0) + return; + + g_bars_M15_check = true; + g_pre_bars_M15 = current_bars_M15; + } + +//+------------------------------------------------------------------+ +//| チャートコメント文字列を作成する関数 +//+------------------------------------------------------------------+ +/** + * @brief チャート左上に表示するステータスメッセージを組み立てます。 + * + * @param ctx 現在のAsk/Bid/スプレッド情報。 + * @return Comment()へ渡す表示用文字列。 + */ +string BuildStatusMessage(TickContext &ctx) + { + string message = StringFormat( + " \nAsk: %." + IntegerToString(ctx.digits) + "f" + "\nBid: %." + IntegerToString(ctx.digits) + "f" + "\nSpread: %." + IntegerToString(ctx.digits) + "f" + "\nSpread Points: %.0f" + "\n\nLast Trend Update:\n %s" + "\n\nMarket State: %d (%s)" + "\n\nLast Target Update:\n %s" + "\n\nMy Used Count: %d / %d" + "\nSplit Zone: %s N=%d ZoneID=%s" + "\n\n[T1 Buy Stop ] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f" + "\n[T2 Buy Limit] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f" + "\n[T3 Sell Stop] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f" + "\n[T4 SellLimit] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f\n", + ctx.ask, ctx.bid, ctx.spread, ctx.spread_points, + TimeToString(g_ea.last_trend_update, TIME_DATE | TIME_MINUTES), + g_ea.trend_state, MarketStateName(g_ea.trend_state), + TimeToString(g_ea.last_target_update, TIME_DATE | TIME_MINUTES), + CountMyUsed(), EffectivePositionLimit(), + (use_split_entry_zone ? "ON" : "OFF"), EffectiveSplitEntryCount(), g_ea.zone_candidate_id, + g_ea.en_price[1], g_ea.tp_price[1], g_ea.sl_price[1], + g_ea.en_price[2], g_ea.tp_price[2], g_ea.sl_price[2], + g_ea.en_price[3], g_ea.tp_price[3], g_ea.sl_price[3], + g_ea.en_price[4], g_ea.tp_price[4], g_ea.sl_price[4] + ); + return message; + } + +//+------------------------------------------------------------------+ +//| チャートコメントを更新する関数 +//+------------------------------------------------------------------+ +/** + * @brief チャート上のステータスコメントを更新します。 + * + * @param ctx 現在のAsk/Bid/スプレッド情報。 + */ +void UpdateStatusComment(TickContext &ctx) + { + Comment(BuildStatusMessage(ctx)); + } + +//+------------------------------------------------------------------+ +//| market_stateの表示名を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief H4 market_stateの人間可読名を返します。 + * + * @param market_state Pythonが出力したH4環境分類(0..5、6は技術エラー停止)。 + * @return 表示用ラベル。 + */ +string MarketStateName(const int market_state) + { + switch(market_state) + { + case MARKET_LOW_VOL_RANGE: + return "LOW_VOL_RANGE"; + case MARKET_HIGH_VOL_RANGE: + return "HIGH_VOL_RANGE"; + case MARKET_LOW_VOL_UP: + return "LOW_VOL_UP"; + case MARKET_HIGH_VOL_UP: + return "HIGH_VOL_UP"; + case MARKET_LOW_VOL_DOWN: + return "LOW_VOL_DOWN"; + case MARKET_HIGH_VOL_DOWN: + return "HIGH_VOL_DOWN"; + case MARKET_TECHNICAL_ERROR_STOP: + return "TECHNICAL_ERROR_STOP"; + default: + return "UNKNOWN"; + } + } + +//+------------------------------------------------------------------+ +//| スプレッドが許容範囲内か判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief 現在スプレッドが入力パラメータの許容範囲内か判定します。 + * + * @param ctx 現在のスプレッド情報。 + * @return 許容範囲内ならtrue、超過している場合はfalse。 + */ +bool IsSpreadAllowed(TickContext &ctx) + { + return (ctx.spread <= spread_limit * Point()); + } + +//+------------------------------------------------------------------+ +//| エントリー価格生成Pythonの完了状態を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief H1エントリー価格生成Pythonの完了状態を確認します。 + * + * @return `process_done_entry.txt` が存在する場合はtrue、未完了の場合はfalse。 + */ +bool IsEntryResultReady() + { + return IsProcessResultReady(done_entry_file, running_entry_file, target_prices_file, "entry", g_entry_process); + } + +//+------------------------------------------------------------------+ +//| ターゲット価格をEA状態へ反映する関数 +//+------------------------------------------------------------------+ +/** + * @brief 完了済みのH1エントリー価格生成結果をEA状態へ反映します。 + * + * @param state EA全体の状態。`res_chk` と各注文タイプのen/tp/slが更新されます。 + */ +void RefreshTargetPrices(EAState &state) + { + GetTargetPrices(state); + } + +//+------------------------------------------------------------------+ + +#endif diff --git a/Include/MyLib/Signals/HITEntrySignal.mqh b/Include/MyLib/Signals/HITEntrySignal.mqh index 857939f..8d11550 100644 --- a/Include/MyLib/Signals/HITEntrySignal.mqh +++ b/Include/MyLib/Signals/HITEntrySignal.mqh @@ -198,6 +198,22 @@ string TargetCandidateAgeText(EAState &state) return IntegerToString(age_seconds) + "s"; } +//+------------------------------------------------------------------+ +//| pending注文コメントへ付与するH1候補IDを返す関数 +//+------------------------------------------------------------------+ +/** + * @brief 復元済みのH1確定足時刻がある場合だけ候補IDをコメントへ付与します。 + */ +string TargetCandidateCommentSuffix(EAState &state) + { + if(state.target_candidate_at <= 0 || + state.zone_candidate_id == "" || + state.zone_candidate_id == "0") + return ""; + + return "Z" + state.zone_candidate_id; + } + //+------------------------------------------------------------------+ //| 許可された注文タイプだけを順番に送信する関数 //+------------------------------------------------------------------+ @@ -267,9 +283,10 @@ string TwoDigit(const int value) //+------------------------------------------------------------------+ //| 分割slot識別子を作る関数 //+------------------------------------------------------------------+ -string SplitSlotKey(const string candidate_id, const int slot_index, const int split_count) +string SplitSlotKey(const int orderType, const string candidate_id, const int slot_index, const int split_count) { - return "Z" + candidate_id + "#" + TwoDigit(slot_index) + "/" + TwoDigit(split_count); + return "Z" + candidate_id + "#T" + IntegerToString(orderType) + "-" + + TwoDigit(slot_index) + "/" + TwoDigit(split_count); } //+------------------------------------------------------------------+ @@ -482,7 +499,7 @@ int TrySendSplitEntryOrders(const int orderType, EAState &state, TickContext &ct break; } - string slot_key = SplitSlotKey(state.zone_candidate_id, slot, split_count); + string slot_key = SplitSlotKey(orderType, state.zone_candidate_id, slot, split_count); if(HasExistingSplitSlot(slot_key)) { Print("[Split Skip] slot already exists. key=", slot_key); @@ -503,7 +520,7 @@ int TrySendSplitEntryOrders(const int orderType, EAState &state, TickContext &ct Print("[Split ", entry_type, " Order Try] key=", slot_key, " en=", en, " tp=", tp, " sl=", sl, " volume=", volume); - if(SendOrder(orderType, en, tp, sl, volume, slot_key)) + if(SendOrder(orderType, en, tp, sl, volume, slot_key, TargetCandidateReferenceTime(state))) { Print("[Split ", entry_type, " Order Sent] key=", slot_key, " en=", en, " tp=", tp, " sl=", sl, " volume=", volume); @@ -568,7 +585,8 @@ bool TrySendEntryOrder(const int orderType, EAState &state, TickContext &ctx) Print("[", entry_type, " Order Try at ", cur_price, "] en=", en, " tp=", tp, " sl=", sl); - if(SendOrder(orderType, en, tp, sl, lot_size, "")) + if(SendOrder(orderType, en, tp, sl, lot_size, + TargetCandidateCommentSuffix(state), TargetCandidateReferenceTime(state))) { Print("[", entry_type, " Order Sent] ticket ok. en=", en, " tp=", tp, " sl=", sl); return true; diff --git a/Include/MyLib/Signals/HITEntrySignal.mqh.bak_codex_20260531_170419 b/Include/MyLib/Signals/HITEntrySignal.mqh.bak_codex_20260531_170419 new file mode 100644 index 0000000..857939f --- /dev/null +++ b/Include/MyLib/Signals/HITEntrySignal.mqh.bak_codex_20260531_170419 @@ -0,0 +1,1070 @@ +#ifndef HIT_ENTRY_SIGNAL_MQH +#define HIT_ENTRY_SIGNAL_MQH + +//| エントリー判定が必要な場合のみ注文処理を実行する関数 +//+------------------------------------------------------------------+ +/** + * @brief エントリー判定タイミングに到達している場合のみ注文処理を実行します。 + * + * @param state EA全体の状態。 + * @param ctx 現在のAsk/Bid/スプレッド情報。 + * + * 判定前チェック、許可注文の送信、リトライ状態更新をまとめて制御します。 + */ +void ProcessEntryDecisionIfNeeded(EAState &state, TickContext &ctx) + { + if(!ShouldRunEntryDecision(state)) + return; + + g_bars_H1_check = false; + g_bars_M15_check = false; + state.last_chk = TimeCurrent(); + + if(!ValidateEntryPreconditions(state)) + return; + + int sent_success = SendAllowedEntryOrders(state, ctx); + UpdateEntryRetryState(state, sent_success); + } + +//+------------------------------------------------------------------+ +//| エントリー判定を実行するタイミングか判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief エントリー判定を実行するタイミングか確認します。 + * + * @param state EA全体の状態。前回判定時刻を参照します。 + * @return H1候補が有効で、M15確定足が更新され、前回判定から一定秒数以上経過していればtrue。 + */ +bool ShouldRunEntryDecision(EAState &state) + { + return (g_bars_H1_check && g_bars_M15_check && TimeCurrent() - state.last_chk >= ENTRY_RETRY_SECONDS); + } + +//+------------------------------------------------------------------+ +//| エントリー判定前の共通チェックを行う関数 +//+------------------------------------------------------------------+ +/** + * @brief 新規注文前の共通条件を検証します。 + * + * @param state EA全体の状態。 + * @return 注文判定を続行できる場合はtrue、停止すべき場合はfalse。 + * + * `res_chk`、market_state、対象EAの注文/ポジション数上限を確認します。 + * market_state=6は相場ボラ停止ではなく、Python/CSV/API失敗時の技術エラー停止として扱います。 + */ +bool ValidateEntryPreconditions(EAState &state) + { + if(use_split_entry_zone) + { + if(state.zone_res_chk != 1) + { + Print("[Entry Skip] target_zones invalid. zone_res_chk=", state.zone_res_chk); + state.chk_cnt = 0; + return false; + } + } + else if(state.res_chk != 1) + { + Print("[Entry Skip] target_prices invalid. res_chk=", state.res_chk); + state.chk_cnt = 0; + return false; + } + + if(state.trend_state < MARKET_LOW_VOL_RANGE || state.trend_state > MARKET_HIGH_VOL_DOWN) + { + Print("[Entry Skip] invalid market_state=", state.trend_state); + state.chk_cnt = 0; + return false; + } + + if(IsTargetCandidateExpired(state)) + { + Print("[Entry Skip] H1 target candidate expired. candidate_at=", + TimeToString(TargetCandidateReferenceTime(state), TIME_DATE | TIME_SECONDS), + " loaded_at=", TimeToString(state.target_loaded_at, TIME_DATE | TIME_SECONDS)); + state.chk_cnt = 0; + return false; + } + + if(IsTargetCandidateTooOldForExecution(state)) + { + Print("[Entry Skip] H1 target candidate is too old for new execution. age=", + TargetCandidateAgeText(state), + " max_age_minutes=", input_entry_max_candidate_age_minutes, + " candidate_at=", TimeToString(TargetCandidateReferenceTime(state), TIME_DATE | TIME_SECONDS), + " loaded_at=", TimeToString(state.target_loaded_at, TIME_DATE | TIME_SECONDS)); + state.chk_cnt = 0; + return false; + } + + int used = CountMyUsed(); + int position_limit = EffectivePositionLimit(); + if(used >= position_limit) + { + Print("Position limit exceeded: used=", used, " limit=", position_limit); + return false; + } + + return true; + } + +//+------------------------------------------------------------------+ +//| H1候補価格の有効期限を判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief H1候補価格がENTRY_H1_LIMIT時間を超えて古くなっていないか判定します。 + * + * @param state EA全体の状態。H1候補価格の元になった確定足時刻を参照します。 + * @return 候補価格が期限切れの場合はtrue。 + */ +bool IsTargetCandidateExpired(EAState &state) + { + datetime reference_time = TargetCandidateReferenceTime(state); + if(reference_time <= 0) + return false; + + int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1); + if(expiration_seconds <= 0) + return false; + + return (TimeCurrent() - reference_time >= expiration_seconds); + } + +//+------------------------------------------------------------------+ +//| H1候補価格の発注許容年齢を判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief M15確認が遅れて整った古いH1候補で新規注文しないように判定します。 + * + * @param state EA全体の状態。H1候補価格の元になった確定足時刻を参照します。 + * @return 入力で指定した最大経過分数を超えている場合はtrue。 + */ +bool IsTargetCandidateTooOldForExecution(EAState &state) + { + if(input_entry_max_candidate_age_minutes <= 0) + return false; + if(TargetCandidateReferenceTime(state) <= 0) + return false; + + int max_age_seconds = input_entry_max_candidate_age_minutes * 60; + if(max_age_seconds <= 0) + return false; + + return (TargetCandidateAgeSeconds(state) > max_age_seconds); + } + +//+------------------------------------------------------------------+ +//| H1候補価格の鮮度判定に使う基準時刻を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief Python結果の読込時刻ではなく、H1確定足由来の候補時刻を優先します。 + */ +datetime TargetCandidateReferenceTime(EAState &state) + { + if(state.target_candidate_at > 0) + return state.target_candidate_at; + + return state.target_loaded_at; + } + +//+------------------------------------------------------------------+ +//| H1候補価格の経過秒数を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief H1候補価格の元になった確定足からの経過秒数を返します。 + */ +int TargetCandidateAgeSeconds(EAState &state) + { + datetime reference_time = TargetCandidateReferenceTime(state); + if(reference_time <= 0) + return -1; + + return (int)(TimeCurrent() - reference_time); + } + +//+------------------------------------------------------------------+ +//| H1候補価格の経過秒数をログ用文字列にする関数 +//+------------------------------------------------------------------+ +/** + * @brief H1候補価格の経過秒数をログへ出しやすい文字列にします。 + */ +string TargetCandidateAgeText(EAState &state) + { + int age_seconds = TargetCandidateAgeSeconds(state); + if(age_seconds < 0) + return "unknown"; + + return IntegerToString(age_seconds) + "s"; + } + +//+------------------------------------------------------------------+ +//| 許可された注文タイプだけを順番に送信する関数 +//+------------------------------------------------------------------+ +/** + * @brief H4 market_stateで許可された注文タイプだけを順番に送信します。 + * + * @param state EA全体の状態。各注文タイプのen/tp/slを参照します。 + * @param ctx 現在のAsk/Bid/スプレッド情報。 + * @return 送信成功した注文数。 + */ +int SendAllowedEntryOrders(EAState &state, TickContext &ctx) + { + if(use_split_entry_zone) + return SendAllowedSplitEntryOrders(state, ctx); + + int sent_success = 0; + + for(int t = 1; t <= 4; t++) + { + int used = CountMyUsed(); + int position_limit = EffectivePositionLimit(); + if(used >= position_limit) + { + Print("Position limit reached while sending. used=", used, " limit=", position_limit); + break; + } + + if(!IsOrderTypeAllowedByTrend(t, state.trend_state)) + { + Print("[Skip] orderType=", t, " not allowed by market_state=", state.trend_state, + " (", MarketStateName(state.trend_state), ")"); + continue; + } + + if(TrySendEntryOrder(t, state, ctx)) + sent_success++; + } + + return sent_success; + } + +//+------------------------------------------------------------------+ +//| 分割本数を安全な範囲に丸める関数 +//+------------------------------------------------------------------+ +int EffectiveSplitEntryCount() + { + int count = split_entry_count; + if(count < 1) + count = 1; + if(count > 10) + count = 10; + + return count; + } + +//+------------------------------------------------------------------+ +//| 2桁番号を作る関数 +//+------------------------------------------------------------------+ +string TwoDigit(const int value) + { + if(value >= 0 && value < 10) + return "0" + IntegerToString(value); + + return IntegerToString(value); + } + +//+------------------------------------------------------------------+ +//| 分割slot識別子を作る関数 +//+------------------------------------------------------------------+ +string SplitSlotKey(const string candidate_id, const int slot_index, const int split_count) + { + return "Z" + candidate_id + "#" + TwoDigit(slot_index) + "/" + TwoDigit(split_count); + } + +//+------------------------------------------------------------------+ +//| ゾーン内の分割価格を返す関数 +//+------------------------------------------------------------------+ +double SplitEntryPrice(const double zone_low, const double zone_high, const int slot_index, const int split_count) + { + int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + if(split_count <= 1) + return NormalizeDouble((zone_low + zone_high) / 2.0, digits); + + double ratio = (double)(slot_index - 1) / (double)(split_count - 1); + return NormalizeDouble(zone_low + (zone_high - zone_low) * ratio, digits); + } + +//+------------------------------------------------------------------+ +//| 注文ロットがブローカー制約を満たすか判定する関数 +//+------------------------------------------------------------------+ +bool IsTradeVolumeAllowed(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("[Split Lot 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("[Split Lot 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("[Split Lot Skip] volume does not match broker step. context=", context, + " volume=", volume, " min=", min_volume, " step=", step_volume); + return false; + } + + return true; + } + +//+------------------------------------------------------------------+ +//| 分割注文1本あたりのロットを計算する関数 +//+------------------------------------------------------------------+ +bool CalcSplitOrderVolume(const int split_count, double &volume) + { + if(split_count <= 0) + return false; + + string context = ""; + if(split_lot_mode == SPLIT_LOT_TOTAL) + { + volume = split_total_lot_size / (double)split_count; + context = "total"; + } + else + { + volume = split_fixed_lot_size; + context = "fixed"; + } + + if(!IsTradeVolumeAllowed(volume, context)) + return false; + + 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++; + } + } + + volume = NormalizeDouble(volume, volume_digits); + return true; + } + +//+------------------------------------------------------------------+ +//| 予測ゾーンが有効値か判定する関数 +//+------------------------------------------------------------------+ +bool HasValidZonePrices(const double zone_low, const double zone_high, const double tp, const double sl) + { + return (zone_low > 0.0 && zone_high > 0.0 && tp > 0.0 && sl > 0.0 && zone_low <= zone_high); + } + +//+------------------------------------------------------------------+ +//| 注文タイプごとのゾーン価格整合条件を判定する関数 +//+------------------------------------------------------------------+ +bool IsTargetZoneOrderConditionMatched(const int orderType, + TickContext &ctx, + const double zone_low, + const double zone_high, + const double tp, + const double sl) + { + switch(orderType) + { + case 1: // Buy Stop + return (ctx.ask < zone_low && tp > zone_high && sl < zone_low); + + case 2: // Buy Limit + return (ctx.ask > zone_high && tp > zone_high && sl < zone_low); + + case 3: // Sell Stop + return (ctx.bid > zone_high && tp < zone_low && sl > zone_high); + + case 4: // Sell Limit + return (ctx.bid < zone_low && tp < zone_low && sl > zone_high); + + default: + return false; + } + } + +//+------------------------------------------------------------------+ +//| 分割ゾーン用の注文送信ループ +//+------------------------------------------------------------------+ +int SendAllowedSplitEntryOrders(EAState &state, TickContext &ctx) + { + int sent_success = 0; + + for(int t = 1; t <= 4; t++) + { + int used = CountMyUsed(); + int position_limit = EffectivePositionLimit(); + if(used >= position_limit) + { + Print("Position limit reached while sending split entries. used=", used, + " limit=", position_limit); + break; + } + + if(!IsOrderTypeAllowedByTrend(t, state.trend_state)) + { + Print("[Split Skip] orderType=", t, " not allowed by market_state=", state.trend_state, + " (", MarketStateName(state.trend_state), ")"); + continue; + } + + sent_success += TrySendSplitEntryOrders(t, state, ctx); + } + + return sent_success; + } + +//+------------------------------------------------------------------+ +//| 1戦略分の分割注文を送信する関数 +//+------------------------------------------------------------------+ +int TrySendSplitEntryOrders(const int orderType, EAState &state, TickContext &ctx) + { + string entry_type = EntryTypeName(orderType); + double cur_price = CurrentPriceForOrderType(orderType, ctx); + double zone_low = state.zone_low[orderType]; + double zone_high = state.zone_high[orderType]; + double tp = state.zone_tp[orderType]; + double sl = state.zone_sl[orderType]; + + if(!HasValidZonePrices(zone_low, zone_high, tp, sl)) + { + Print("[Split Skip] invalid target zone. orderType=", orderType, + " zone=", zone_low, "-", zone_high, " tp=", tp, " sl=", sl, + " candidate_id=", state.zone_candidate_id); + return 0; + } + + if(!IsTargetZoneOrderConditionMatched(orderType, ctx, zone_low, zone_high, tp, sl)) + { + Print("[No Split ", entry_type, "] cur=", cur_price, + " zone=", zone_low, "-", zone_high, " tp=", tp, " sl=", sl, + " candidate_id=", state.zone_candidate_id); + return 0; + } + + if(!IsM15ZoneTimingConfirmed(orderType, ctx, zone_low, zone_high)) + { + Print("[No Split ", entry_type, "] M15 zone timing not confirmed. cur=", cur_price, + " zone=", zone_low, "-", zone_high, + " candidate_id=", state.zone_candidate_id); + return 0; + } + + int split_count = EffectiveSplitEntryCount(); + double volume = 0.0; + if(!CalcSplitOrderVolume(split_count, volume)) + return 0; + + int sent_success = 0; + for(int slot = 1; slot <= split_count; slot++) + { + int used = CountMyUsed(); + int position_limit = EffectivePositionLimit(); + if(used >= position_limit) + { + Print("Position limit reached during split slots. used=", used, + " limit=", position_limit); + break; + } + + string slot_key = SplitSlotKey(state.zone_candidate_id, slot, split_count); + if(HasExistingSplitSlot(slot_key)) + { + Print("[Split Skip] slot already exists. key=", slot_key); + continue; + } + + double en = SplitEntryPrice(zone_low, zone_high, slot, split_count); + if(!IsTargetPriceOrderConditionMatched(orderType, ctx, en, tp, sl)) + { + Print("[Split Skip] slot price no longer valid. key=", slot_key, + " cur=", cur_price, " en=", en, " tp=", tp, " sl=", sl); + continue; + } + + if(!MeetsTradeDistanceRules(orderType, ctx, en, tp, sl)) + continue; + + Print("[Split ", entry_type, " Order Try] key=", slot_key, + " en=", en, " tp=", tp, " sl=", sl, " volume=", volume); + + if(SendOrder(orderType, en, tp, sl, volume, slot_key)) + { + Print("[Split ", entry_type, " Order Sent] key=", slot_key, + " en=", en, " tp=", tp, " sl=", sl, " volume=", volume); + sent_success++; + } + else + { + Print("[Split ", entry_type, " Order Failed] key=", slot_key, + " en=", en, " tp=", tp, " sl=", sl, " volume=", volume); + } + } + + return sent_success; + } + +//+------------------------------------------------------------------+ +//| 注文タイプ1件分の価格検証と注文送信を行う関数 +//+------------------------------------------------------------------+ +/** + * @brief 注文タイプ1件分の価格検証と注文送信を行います。 + * + * @param orderType 注文タイプ。1=Buy Stop、2=Buy Limit、3=Sell Stop、4=Sell Limit。 + * @param state EA全体の状態。対象注文タイプのen/tp/slを参照します。 + * @param ctx 現在のAsk/Bid情報。 + * @return 注文送信に成功した場合はtrue、それ以外はfalse。 + */ +bool TrySendEntryOrder(const int orderType, EAState &state, TickContext &ctx) + { + string entry_type = EntryTypeName(orderType); + double cur_price = CurrentPriceForOrderType(orderType, ctx); + + double en = state.en_price[orderType]; + double tp = state.tp_price[orderType]; + double sl = state.sl_price[orderType]; + + if(!HasValidTargetPrices(en, tp, sl)) + { + Print("[Skip] invalid target prices. orderType=", orderType, + " en=", en, " tp=", tp, " sl=", sl, + " candidate_age=", TargetCandidateAgeText(state)); + return false; + } + + bool ok = IsTargetPriceOrderConditionMatched(orderType, ctx, en, tp, sl); + if(!ok) + { + Print("[No ", entry_type, "] cur=", cur_price, " en=", en, " tp=", tp, " sl=", sl, + " candidate_age=", TargetCandidateAgeText(state)); + return false; + } + + if(!IsM15EntryTimingConfirmed(orderType, ctx, en)) + { + Print("[No ", entry_type, "] M15 timing not confirmed. cur=", cur_price, " en=", en, + " candidate_age=", TargetCandidateAgeText(state), + " max_age_minutes=", input_entry_max_candidate_age_minutes); + return false; + } + + if(!MeetsTradeDistanceRules(orderType, ctx, en, tp, sl)) + return false; + + Print("[", entry_type, " Order Try at ", cur_price, "] en=", en, " tp=", tp, " sl=", sl); + + if(SendOrder(orderType, en, tp, sl, lot_size, "")) + { + Print("[", entry_type, " Order Sent] ticket ok. en=", en, " tp=", tp, " sl=", sl); + return true; + } + + Print("[", entry_type, " Order Failed] en=", en, " tp=", tp, " sl=", sl); + return false; + } + +//+------------------------------------------------------------------+ +//| 注文タイプ名を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief 注文タイプ番号に対応する表示名を返します。 + * + * @param orderType 注文タイプ番号。 + * @return 注文タイプの表示名。不正値の場合は"Unknown"。 + */ +string EntryTypeName(const int orderType) + { + switch(orderType) + { + case 1: + return "Buy Stop"; + case 2: + return "Buy Limit"; + case 3: + return "Sell Stop"; + case 4: + return "Sell Limit"; + default: + return "Unknown"; + } + } + +//+------------------------------------------------------------------+ +//| 注文タイプに応じた現在価格を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief 注文タイプに応じて価格比較に使う現在価格を返します。 + * + * @param orderType 注文タイプ番号。 + * @param ctx 現在のAsk/Bid情報。 + * @return 買い系注文ではAsk、売り系注文ではBid。 + */ +double CurrentPriceForOrderType(const int orderType, TickContext &ctx) + { + if(orderType == 1 || orderType == 2) + return ctx.ask; + + return ctx.bid; + } + +//+------------------------------------------------------------------+ +//| 注文タイプごとの価格整合条件を判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief 注文タイプごとの現在価格・エントリー・TP・SLの大小関係を検証します。 + * + * @param orderType 注文タイプ番号。 + * @param ctx 現在のAsk/Bid情報。 + * @param en エントリー価格。 + * @param tp 利確価格。 + * @param sl 損切価格。 + * @return 注文タイプの価格条件を満たす場合はtrue。 + */ +bool IsTargetPriceOrderConditionMatched(const int orderType, TickContext &ctx, const double en, const double tp, const double sl) + { + switch(orderType) + { + case 1: // Buy Stop + return (ctx.ask < en && tp > en && sl < en); + + case 2: // Buy Limit + return (ctx.ask > en && tp > en && sl < en); + + case 3: // Sell Stop + return (ctx.bid > en && tp < en && sl > en); + + case 4: // Sell Limit + return (ctx.bid < en && tp < en && sl > en); + + default: + return false; + } + } + +//+------------------------------------------------------------------+ +//| 順張りStop注文タイプか判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief M15反転確認を待たずH1候補で発注する順張りStop注文か判定します。 + * + * @param orderType 注文タイプ番号。 + * @return Buy Stop または Sell Stop の場合はtrue。 + */ +bool IsTrendStopOrderType(const int orderType) + { + return (orderType == 1 || orderType == 3); + } + +//+------------------------------------------------------------------+ +//| M15確定足によるエントリータイミング確認 +//+------------------------------------------------------------------+ +/** + * @brief H1候補価格に対してM15の発注タイミングが整っているか判定します。 + * + * @param orderType 注文タイプ番号。 + * @param ctx 現在のAsk/Bid情報。 + * @param en H1で決めたエントリー候補価格。 + * @return M15確認条件を満たす場合はtrue。 + * + * H4/H1の方向判断は維持し、M15では「候補価格に近い」「直近確定足が + * 順張り/反転の根拠を持つ」ことだけを確認します。 + */ +bool IsM15EntryTimingConfirmed(const int orderType, TickContext &ctx, const double en) + { + if(!use_m15_entry_filter) + return true; + + if(IsTrendStopOrderType(orderType) && !use_m15_imbalance_confirmation) + return true; + + MqlRates rates[]; + int copied = CopyRates(_Symbol, PERIOD_M15, OHLC_START_SHIFT, M15_CONFIRM_BARS, rates); + if(copied < 3) + { + Print("[M15 Filter] insufficient M15 bars. copied=", copied); + return false; + } + + int last_index = copied - 1; + int prev_index = copied - 2; + + if(IsTrendStopOrderType(orderType)) + return IsM15ImbalanceConfirmationPassed(orderType, rates, copied, last_index); + + double avg_range = AverageM15Range(rates, copied); + double min_zone = M15_MIN_ENTRY_ZONE_POINTS * Point(); + double entry_zone = avg_range * m15_entry_zone_atr_multiplier; + if(entry_zone < min_zone) + entry_zone = min_zone; + + double cur_price = CurrentPriceForOrderType(orderType, ctx); + if(MathAbs(cur_price - en) > entry_zone) + { + Print("[M15 Filter] ", EntryTypeName(orderType), " is not near entry zone. cur=", + cur_price, " en=", en, " zone=", entry_zone); + return false; + } + + if(!IsM15SignalAligned(orderType, rates[prev_index], rates[last_index], en, entry_zone)) + return false; + + return IsM15ImbalanceConfirmationPassed(orderType, rates, copied, last_index); + } + +//+------------------------------------------------------------------+ +//| M15確定足によるゾーンエントリータイミング確認 +//+------------------------------------------------------------------+ +/** + * @brief H1予測ゾーンに対してM15の発注タイミングが整っているか判定します。 + */ +bool IsM15ZoneTimingConfirmed(const int orderType, + TickContext &ctx, + const double zone_low, + const double zone_high) + { + if(!use_m15_entry_filter) + return true; + + if(IsTrendStopOrderType(orderType) && !use_m15_imbalance_confirmation) + return true; + + MqlRates rates[]; + int copied = CopyRates(_Symbol, PERIOD_M15, OHLC_START_SHIFT, M15_CONFIRM_BARS, rates); + if(copied < 3) + { + Print("[M15 Zone Filter] insufficient M15 bars. copied=", copied); + return false; + } + + int last_index = copied - 1; + int prev_index = copied - 2; + + if(IsTrendStopOrderType(orderType)) + return IsM15ImbalanceConfirmationPassed(orderType, rates, copied, last_index); + + double avg_range = AverageM15Range(rates, copied); + double min_zone = M15_MIN_ENTRY_ZONE_POINTS * Point(); + double entry_zone = avg_range * m15_entry_zone_atr_multiplier; + if(entry_zone < min_zone) + entry_zone = min_zone; + + double cur_price = CurrentPriceForOrderType(orderType, ctx); + if(cur_price < zone_low - entry_zone || cur_price > zone_high + entry_zone) + { + Print("[M15 Zone Filter] ", EntryTypeName(orderType), " is not near target zone. cur=", + cur_price, " zone=", zone_low, "-", zone_high, " padding=", entry_zone); + return false; + } + + double representative = (zone_low + zone_high) / 2.0; + double expanded_zone = entry_zone + (zone_high - zone_low) / 2.0; + if(!IsM15SignalAligned(orderType, rates[prev_index], rates[last_index], representative, expanded_zone)) + return false; + + return IsM15ImbalanceConfirmationPassed(orderType, rates, copied, last_index); + } + +//+------------------------------------------------------------------+ +//| M15平均レンジを計算する関数 +//+------------------------------------------------------------------+ +/** + * @brief M15確定足の平均レンジを計算します。 + * + * @param rates M15のMqlRates配列。 + * @param count 使用する本数。 + * @return 平均レンジ。算出不能な場合は最小ゾーン幅を返します。 + */ +double AverageM15Range(const MqlRates &rates[], const int count) + { + double total_range = 0.0; + int used = 0; + + for(int i = 0; i < count; i++) + { + double range = rates[i].high - rates[i].low; + if(range <= 0.0) + continue; + + total_range += range; + used++; + } + + if(used <= 0) + return M15_MIN_ENTRY_ZONE_POINTS * Point(); + + return total_range / used; + } + +//+------------------------------------------------------------------+ +//| M15平均実体を計算する関数 +//+------------------------------------------------------------------+ +/** + * @brief 判定対象足を含めず、直前N本のM15平均実体サイズを返します。 + * + * @param rates M15のMqlRates配列。 + * @param current_index 判定対象の配列index。 + * @param period 平均計算本数。 + * @return 平均実体サイズ。算出不能な場合は0。 + */ +double AverageM15BodySize(const MqlRates &rates[], const int current_index, const int period) + { + if(period <= 0) + return 0.0; + if(current_index < period) + return 0.0; + + double total_body = 0.0; + for(int i = current_index - period; i < current_index; i++) + total_body += MathAbs(rates[i].close - rates[i].open); + + return total_body / period; + } + +//+------------------------------------------------------------------+ +//| M15初動確認を行う注文タイプか判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief 初動フォローの追加確認を適用する注文タイプを返します。 + */ +bool RequiresM15ImbalanceConfirmation(const int orderType) + { + return IsTrendStopOrderType(orderType); + } + +//+------------------------------------------------------------------+ +//| M15インバランス初動確認 +//+------------------------------------------------------------------+ +/** + * @brief T1/T3順張り注文に対してM15確定足の初動またはブレイクを確認します。 + * + * H1/Pythonで決めた方向は上書きせず、発注直前にM15の方向一致と勢いだけを確認します。 + */ +bool IsM15ImbalanceConfirmationPassed(const int orderType, const MqlRates &rates[], const int count, const int current_index) + { + if(!use_m15_imbalance_confirmation) + return true; + if(!RequiresM15ImbalanceConfirmation(orderType)) + return true; + + if(m15_imbalance_avg_body_period <= 0 || m15_imbalance_sensitivity <= 0.0) + { + Print("[M15 Imbalance] invalid settings. period=", m15_imbalance_avg_body_period, + " sensitivity=", m15_imbalance_sensitivity); + return false; + } + + if(count <= m15_imbalance_avg_body_period || current_index <= 0) + { + Print("[M15 Imbalance] insufficient bars. copied=", count, + " period=", m15_imbalance_avg_body_period); + return false; + } + + double avg_body = AverageM15BodySize(rates, current_index, m15_imbalance_avg_body_period); + double min_avg_body = m15_imbalance_min_avg_body_points * Point(); + double current_body = MathAbs(rates[current_index].close - rates[current_index].open); + + if(avg_body <= min_avg_body) + { + if(use_m15_imbalance_debug_log) + Print("[M15 Imbalance] average body too small. avg=", avg_body, + " min=", min_avg_body, " orderType=", orderType); + return false; + } + + bool bullish = (rates[current_index].close > rates[current_index].open); + bool bearish = (rates[current_index].close < rates[current_index].open); + bool direction_ok = (orderType == 1 && bullish) || (orderType == 3 && bearish); + bool body_ok = (current_body > avg_body * m15_imbalance_sensitivity); + bool break_ok = false; + + if(orderType == 1) + break_ok = (rates[current_index].close > rates[current_index - 1].high); + else if(orderType == 3) + break_ok = (rates[current_index].close < rates[current_index - 1].low); + + bool passed = (direction_ok && (body_ok || break_ok)); + if(use_m15_imbalance_debug_log) + { + Print("[M15 Imbalance] orderType=", orderType, + " avg_body=", avg_body, + " current_body=", current_body, + " sensitivity=", m15_imbalance_sensitivity, + " direction_ok=", direction_ok, + " body_ok=", body_ok, + " break_ok=", break_ok, + " passed=", passed); + } + + return passed; + } + +//+------------------------------------------------------------------+ +//| 注文タイプごとのM15シグナル方向を判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief 注文タイプごとにM15確定足の勢い・反転根拠を確認します。 + * + * @param orderType 注文タイプ番号。 + * @param prev_bar 1本前のM15確定足。 + * @param last_bar 直近のM15確定足。 + * @param en H1で決めたエントリー候補価格。 + * @param entry_zone M15平均レンジから算出した候補価格付近の許容幅。 + * @return 注文タイプに沿ったM15根拠があればtrue。 + */ +bool IsM15SignalAligned(const int orderType, const MqlRates &prev_bar, const MqlRates &last_bar, const double en, const double entry_zone) + { + double range = SafeBarRange(last_bar); + double body_ratio = MathAbs(last_bar.close - last_bar.open) / range; + double upper_wick_ratio = (last_bar.high - MathMax(last_bar.open, last_bar.close)) / range; + double lower_wick_ratio = (MathMin(last_bar.open, last_bar.close) - last_bar.low) / range; + + bool bullish = (last_bar.close > last_bar.open); + bool bearish = (last_bar.close < last_bar.open); + bool strong_body = (body_ratio >= M15_MIN_BODY_RATIO); + bool bullish_break = (last_bar.close > prev_bar.high); + bool bearish_break = (last_bar.close < prev_bar.low); + bool lower_rejection = (lower_wick_ratio >= M15_REJECTION_WICK_RATIO); + bool upper_rejection = (upper_wick_ratio >= M15_REJECTION_WICK_RATIO); + + switch(orderType) + { + case 1: // Buy Stop: M15の上方向モメンタムを確認 + return (bullish && (bullish_break || strong_body) && last_bar.close <= en + entry_zone); + + case 2: // Buy Limit: 候補価格付近で下ヒゲ反転または買い戻しを確認 + return (last_bar.low <= en + entry_zone && bullish && + (lower_rejection || bullish_break || strong_body)); + + case 3: // Sell Stop: M15の下方向モメンタムを確認 + return (bearish && (bearish_break || strong_body) && last_bar.close >= en - entry_zone); + + case 4: // Sell Limit: 候補価格付近で上ヒゲ反転または売り戻しを確認 + return (last_bar.high >= en - entry_zone && bearish && + (upper_rejection || bearish_break || strong_body)); + + default: + return false; + } + } + +//+------------------------------------------------------------------+ +//| ローソク足レンジを安全に取得する関数 +//+------------------------------------------------------------------+ +/** + * @brief ゼロ除算を避けるため、最小値を持つローソク足レンジを返します。 + * + * @param bar 対象ローソク足。 + * @return high-low。0以下の場合はPoint()を返します。 + */ +double SafeBarRange(const MqlRates &bar) + { + double range = bar.high - bar.low; + if(range <= 0.0) + return Point(); + + return range; + } + +//+------------------------------------------------------------------+ +//| brokerの最小距離制約を満たすか判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief pending価格、TP、SLがstop level / freeze levelの最小距離を満たすか判定します。 + * + * @param orderType 注文タイプ番号。 + * @param ctx 現在のAsk/Bid情報。 + * @param en エントリー価格。 + * @param tp 利確価格。 + * @param sl 損切価格。 + * @return 最小距離を満たす場合はtrue。 + */ +bool MeetsTradeDistanceRules(const int orderType, TickContext &ctx, const double en, const double tp, const double sl) + { + int stop_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + int freeze_level = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL); + int min_level = stop_level; + if(freeze_level > min_level) + min_level = freeze_level; + double min_distance = min_level * Point(); + + if(min_distance <= 0.0) + return true; + + double cur_price = CurrentPriceForOrderType(orderType, ctx); + string entry_type = EntryTypeName(orderType); + + if(MathAbs(en - cur_price) < min_distance) + { + Print("[Skip] ", entry_type, " entry is too close. cur=", cur_price, + " en=", en, " min_distance=", min_distance); + return false; + } + + if(MathAbs(tp - en) < min_distance) + { + Print("[Skip] ", entry_type, " TP is too close. en=", en, + " tp=", tp, " min_distance=", min_distance); + return false; + } + + if(MathAbs(en - sl) < min_distance) + { + Print("[Skip] ", entry_type, " SL is too close. en=", en, + " sl=", sl, " min_distance=", min_distance); + return false; + } + + return true; + } + +//+------------------------------------------------------------------+ +//| 注文送信結果に応じてリトライ状態を更新する関数 +//+------------------------------------------------------------------+ +/** + * @brief 注文送信結果に応じてエントリー判定のリトライ状態を更新します。 + * + * @param state EA全体の状態。`chk_cnt` を更新します。 + * @param sent_success 今回送信に成功した注文数。 + * + * 注文が1件も送信されなかった場合、最大10回まで60秒間隔で再判定します。 + */ +void UpdateEntryRetryState(EAState &state, const int sent_success) + { + if(sent_success > 0) + { + state.chk_cnt = 0; + return; + } + + state.chk_cnt += 1; + Print("[Retry] no order sent. chk_cnt=", state.chk_cnt, "/", ENTRY_RETRY_LIMIT, + " (wait next M15 bar)"); + + if(state.chk_cnt < ENTRY_RETRY_LIMIT) + { + g_bars_H1_check = true; // 次のM15確定足で再度エントリー判定を実行する + return; + } + + Print("[Retry End] reached max tries. reset chk_cnt."); + state.chk_cnt = 0; + } + + +#endif diff --git a/Include/MyLib/Signals/HITPythonSignalGateway.mqh b/Include/MyLib/Signals/HITPythonSignalGateway.mqh index 882a28f..2702504 100644 --- a/Include/MyLib/Signals/HITPythonSignalGateway.mqh +++ b/Include/MyLib/Signals/HITPythonSignalGateway.mqh @@ -218,25 +218,23 @@ bool TryParseStrictDouble(const string value, double &parsed) //| H1候補IDを安全な12桁時刻トークンへ正規化する関数 //+------------------------------------------------------------------+ /** - * @brief Pythonから受け取ったcandidate_idを注文コメントに使える数字だけへ丸めます。 + * @brief Pythonから受け取ったcandidate_idが厳格な12桁数字の場合だけ採用します。 */ string NormalizeTargetCandidateId(const string value) { string text = TrimText(value); - string output = ""; int length = StringLen(text); - - for(int i = 0; i < length && StringLen(output) < 12; i++) - { - int ch = StringGetCharacter(text, i); - if(IsDecimalDigit(ch)) - output += StringSubstr(text, i, 1); - } - - if(StringLen(output) <= 0) + if(length != 12) return "0"; - return output; + for(int i = 0; i < length; i++) + { + int ch = StringGetCharacter(text, i); + if(!IsDecimalDigit(ch)) + return "0"; + } + + return text; } //+------------------------------------------------------------------+ @@ -406,11 +404,13 @@ bool GetTrendState(EAState &state) if(ProcessDone && g_ea.load_trend_flg) { int trend_state; - LoadTrendState(trend_state); - state.trend_state = trend_state; - Print("market_state: ", state.trend_state, " (", MarketStateName(state.trend_state), ")"); - g_ea.load_trend_flg = false; - g_ea.last_trend_update = TimeLocal(); + LoadTrendState(trend_state); + state.trend_state = trend_state; + Print("market_state: ", state.trend_state, " (", MarketStateName(state.trend_state), ")"); + CancelPendingOrdersNotAllowedByTrend(state.trend_state); + g_init_entry_pending = true; // Rebuild H1 candidates from the refreshed H4 state. + g_ea.load_trend_flg = false; + g_ea.last_trend_update = TimeLocal(); } return true; } @@ -685,6 +685,13 @@ void LoadTargetZones(EAState &state) datetime candidate_at = 0; if(TryParseTargetCandidateTime(state.zone_candidate_id, candidate_at)) state.target_candidate_at = candidate_at; + else if(state.zone_res_chk == 1) + { + Print("target_zones invalid candidate_id: ", line); + FileClose(filehandle); + ResetTargetZones(state); + return; + } else state.target_candidate_at = 0; continue; diff --git a/Include/MyLib/Signals/HITPythonSignalGateway.mqh.bak_codex_20260531_170419 b/Include/MyLib/Signals/HITPythonSignalGateway.mqh.bak_codex_20260531_170419 new file mode 100644 index 0000000..882a28f --- /dev/null +++ b/Include/MyLib/Signals/HITPythonSignalGateway.mqh.bak_codex_20260531_170419 @@ -0,0 +1,725 @@ +#ifndef HIT_PYTHON_SIGNAL_GATEWAY_MQH +#define HIT_PYTHON_SIGNAL_GATEWAY_MQH + +//+------------------------------------------------------------------+ +//| 最新のOHLCデータを取得する関数 +//+------------------------------------------------------------------+ +/** + * @brief 指定時間足のOHLCデータを取得します。 + * + * @param times 取得したバー時刻を格納する配列。 + * @param open_prices 取得したOpen価格を格納する配列。 + * @param high_prices 取得したHigh価格を格納する配列。 + * @param low_prices 取得したLow価格を格納する配列。 + * @param close_prices 取得したClose価格を格納する配列。 + * @param tf 取得対象の時間足。 + * @param bars_count 取得するバー本数。 + * @return 取得に成功した場合はtrue、失敗した場合はfalse。 + * + * `OHLC_START_SHIFT=1` のため、形成中のバーではなく確定足から取得します。 + */ +bool GetLatestOHLC(datetime ×[], double &open_prices[], double &high_prices[], double &low_prices[], double &close_prices[], ENUM_TIMEFRAMES tf, int bars_count) + { + ArrayResize(times, 0); + ArrayResize(open_prices, 0); + ArrayResize(high_prices, 0); + ArrayResize(low_prices, 0); + ArrayResize(close_prices, 0); + + if(bars_count < 2) + return false; + +// OHLCデータを取得 +// CopyTime と CopyRates を別々に呼ばず、MqlRates の time/open/high/low/close を同一配列から取得する。 +// OHLC_START_SHIFT=1 のため、形成中の0本目ではなく確定足からPythonへ渡す。 + MqlRates rates[]; + int copied = CopyRates(_Symbol, tf, OHLC_START_SHIFT, bars_count, rates); + if(copied <= 0) + { + Print(__FUNCTION__, ": Failed to copy rates data (bars=", bars_count, ") err=", GetLastError()); + return false; + } + + ArrayResize(times, copied); + ArrayResize(open_prices, copied); + ArrayResize(high_prices, copied); + ArrayResize(low_prices, copied); + ArrayResize(close_prices, copied); + +// 必要なデータを配列に格納 + for(int i = 0; i < copied; i++) + { + times[i] = rates[i].time; + open_prices[i] = rates[i].open; + high_prices[i] = rates[i].high; + low_prices[i] = rates[i].low; + close_prices[i] = rates[i].close; + } + + return true; + } + +//+------------------------------------------------------------------+ +//| "ohlc.csv"を出力する関数 +//+------------------------------------------------------------------+ +/** + * @brief OHLC配列をPython入力用CSVとして出力します。 + * + * @param filename 出力するCSVファイル名。 + * @param times バー時刻配列。 + * @param open_prices Open価格配列。 + * @param high_prices High価格配列。 + * @param low_prices Low価格配列。 + * @param close_prices Close価格配列。 + * @return CSV出力に成功した場合はtrue。 + */ +bool RecordOHLC(const string filename, const datetime ×[], const double &open_prices[], const double &high_prices[], const double &low_prices[], const double &close_prices[]) + { +// 出力先ファイル名 +// string filename = "ohlc.csv"; + +// ファイルを "書き込みモード" でオープン (テキスト/ANSI) + int fileHandle = FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI); + +// ファイルが開けたかチェック + if(fileHandle == INVALID_HANDLE) + { + Print(__FUNCTION__, " : Failed to open file: ", GetLastError()); + return false; + } + +// 配列サイズを取得 (times, open, high, low, close の要素数は同じ前提) + int size = ArraySize(times); + +// ヘッダー行を追加 + FileWrite(fileHandle, "Time,Open,High,Low,Close"); + +// 小数点の桁数を取得 + int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + +// フォーマット文字列を動的に生成 + string formatString = StringFormat("%%s,%%.%df,%%.%df,%%.%df,%%.%df", digits, digits, digits, digits); + +// 1行ずつ "時刻, Open, High, Low, Close" の形式で書き込み + for(int i = 0; i < size; i++) + { + // 時間をフォーマット + string timeStr = TimeToString(times[i], TIME_DATE | TIME_MINUTES); + + // 1行分の文字列を生成 (小数点以下の桁数を `digits` に調整) + string line = StringFormat(formatString, + timeStr, open_prices[i], high_prices[i], low_prices[i], close_prices[i]); + + // ファイルに書き込み (改行付き) + FileWrite(fileHandle, line); + } + +// 書き込み終了後、ファイルを閉じる + FileClose(fileHandle); + return true; + } + +//+------------------------------------------------------------------+ +//| 文字列の前後空白を除去する関数 +//+------------------------------------------------------------------+ +/** + * @brief Python連携ファイルから読んだ1行をパース前に正規化します。 + */ +string TrimText(const string value) + { + string text = value; + StringTrimLeft(text); + StringTrimRight(text); + return text; + } + +//+------------------------------------------------------------------+ +//| 10進数字か判定する関数 +//+------------------------------------------------------------------+ +bool IsDecimalDigit(const int ch) + { + return (ch >= 48 && ch <= 57); + } + +//+------------------------------------------------------------------+ +//| 厳格な整数パース +//+------------------------------------------------------------------+ +/** + * @brief 数字だけで構成された整数行を読み込みます。不正文字があればfalse。 + */ +bool TryParseStrictInteger(const string value, int &parsed) + { + string text = TrimText(value); + int length = StringLen(text); + if(length <= 0) + return false; + + for(int i = 0; i < length; i++) + { + if(!IsDecimalDigit(StringGetCharacter(text, i))) + return false; + } + + parsed = (int)StringToInteger(text); + return true; + } + +//+------------------------------------------------------------------+ +//| 厳格な小数パース +//+------------------------------------------------------------------+ +/** + * @brief 単純な10進数行を読み込みます。指数表記や余計な文字は許可しません。 + */ +bool TryParseStrictDouble(const string value, double &parsed) + { + string text = TrimText(value); + int length = StringLen(text); + if(length <= 0) + return false; + + bool has_digit = false; + bool has_dot = false; + int start_index = 0; + + int first_char = StringGetCharacter(text, 0); + if(first_char == 43 || first_char == 45) // + or - + { + if(length == 1) + return false; + start_index = 1; + } + + for(int i = start_index; i < length; i++) + { + int ch = StringGetCharacter(text, i); + if(IsDecimalDigit(ch)) + { + has_digit = true; + continue; + } + + if(ch == 46 && !has_dot) // dot + { + has_dot = true; + continue; + } + + return false; + } + + if(!has_digit) + return false; + + parsed = StringToDouble(text); + return true; + } + +//+------------------------------------------------------------------+ +//| H1候補IDを安全な12桁時刻トークンへ正規化する関数 +//+------------------------------------------------------------------+ +/** + * @brief Pythonから受け取ったcandidate_idを注文コメントに使える数字だけへ丸めます。 + */ +string NormalizeTargetCandidateId(const string value) + { + string text = TrimText(value); + string output = ""; + int length = StringLen(text); + + for(int i = 0; i < length && StringLen(output) < 12; i++) + { + int ch = StringGetCharacter(text, i); + if(IsDecimalDigit(ch)) + output += StringSubstr(text, i, 1); + } + + if(StringLen(output) <= 0) + return "0"; + + return output; + } + +//+------------------------------------------------------------------+ +//| H1候補IDから確定足時刻を復元する関数 +//+------------------------------------------------------------------+ +/** + * @brief `yyyyMMddHHmm` 形式のcandidate_idをdatetimeへ変換します。 + */ +bool TryParseTargetCandidateTime(const string candidate_id, datetime &candidate_at) + { + candidate_at = 0; + string text = NormalizeTargetCandidateId(candidate_id); + if(StringLen(text) != 12) + return false; + + int year = (int)StringToInteger(StringSubstr(text, 0, 4)); + int month = (int)StringToInteger(StringSubstr(text, 4, 2)); + int day = (int)StringToInteger(StringSubstr(text, 6, 2)); + int hour = (int)StringToInteger(StringSubstr(text, 8, 2)); + int minute = (int)StringToInteger(StringSubstr(text, 10, 2)); + + if(year < 2000 || month < 1 || month > 12 || + day < 1 || day > 31 || hour < 0 || hour > 23 || + minute < 0 || minute > 59) + return false; + + string datetime_text = StringFormat("%04d.%02d.%02d %02d:%02d", + year, month, day, hour, minute); + datetime parsed = StringToTime(datetime_text); + if(parsed <= 0) + return false; + + MqlDateTime parts; + TimeToStruct(parsed, parts); + if(parts.year != year || parts.mon != month || parts.day != day || + parts.hour != hour || parts.min != minute) + return false; + + candidate_at = parsed; + return true; + } + +//+------------------------------------------------------------------+ +//| バッチファイル(Pythonスクリプト)を実行する関数 +//+------------------------------------------------------------------+ +/** + * @brief H4トレンド判定用バッチファイルを起動します。 + * + * @return 起動に成功した場合はtrue、Python実行中または起動失敗時はfalse。 + * + * 起動直前に`process_done_trend.txt`を削除し、プロセスハンドルを保持して終了確認できる状態にします。 + */ +bool ExecuteBatchTrend() + { + if(!IsProcessStartAllowed(done_trend_file, running_trend_file, "trend", g_trend_process)) + return false; + + DeleteDoneFile(done_trend_file); + int price_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + return StartBatchProcess(get_trend_reply_bat, running_trend_file, "trend", + g_trend_process, mt5_file_prefix, price_digits); + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief H1エントリー価格生成用バッチファイルを起動します。 + * + * @return 起動に成功した場合はtrue、Python実行中または起動失敗時はfalse。 + * + * 起動直前に`process_done_entry.txt`を削除し、プロセスハンドルを保持して終了確認できる状態にします。 + */ +bool ExecuteBatchEntry() + { + if(!IsProcessStartAllowed(done_entry_file, running_entry_file, "entry", g_entry_process)) + return false; + + DeleteDoneFile(done_entry_file); + int price_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + return StartBatchProcess(get_entry_reply_bat, running_entry_file, "entry", + g_entry_process, mt5_file_prefix, price_digits); + } + + +//+------------------------------------------------------------------+ +//| "ohlc.csv"を出力後、バッチファイルの実行する関数 +//+------------------------------------------------------------------+ +/** + * @brief H4 OHLCをCSV出力し、トレンド判定Pythonを起動します。 + * + * @param state EA全体の状態。トレンド読込待ちフラグを更新します。 + * @return CSV出力とバッチ起動が開始できた場合はtrue。 + */ +bool RecordOHLCAndExecuteBatch_Trend(EAState &state) + { + if(!IsProcessStartAllowed(done_trend_file, running_trend_file, "trend", g_trend_process)) + { + Print("Trend Python is still running. Skip H4 CSV update."); + return false; + } + + datetime times[]; + double open_prices[], high_prices[], low_prices[], close_prices[]; + + if(!GetLatestOHLC(times, open_prices, high_prices, low_prices, close_prices, + PERIOD_H4, HISTORY_BARS)) + { Print("GetLatestOHLC(H4) failed."); return false; } + + if(!RecordOHLC(ohlc_h4_file, times, open_prices, high_prices, low_prices, close_prices)) + { Print("RecordOHLC(H4) failed."); return false; } + + if(!ExecuteBatchTrend()) + return false; + + state.load_trend_flg = true; + return true; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief H1 OHLCをCSV出力し、エントリー価格生成Pythonを起動します。 + * + * @param state EA全体の状態。ターゲット価格読込待ちフラグを更新します。 + * @return CSV出力とバッチ起動が開始できた場合はtrue。 + */ +bool RecordOHLCAndExecuteBatch_Entry(EAState &state) + { + if(!IsProcessStartAllowed(done_entry_file, running_entry_file, "entry", g_entry_process)) + { + Print("Entry Python is still running. Skip H1 CSV update."); + return false; + } + + datetime times[]; + double open_prices[], high_prices[], low_prices[], close_prices[]; + + if(!GetLatestOHLC(times, open_prices, high_prices, low_prices, close_prices, + PERIOD_H1, HISTORY_BARS)) + { Print("GetLatestOHLC(H1) failed."); return false; } + + if(!RecordOHLC(ohlc_h1_file, times, open_prices, high_prices, low_prices, close_prices)) + { Print("RecordOHLC(H1) failed."); return false; } + + if(!ExecuteBatchEntry()) + return false; + + state.load_target_flg = true; + return true; + } + + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief Pythonが出力したトレンド判定結果を必要なタイミングで読み込みます。 + * + * @param state EA全体の状態。H4 market_stateと更新フラグを更新します。 + * @return 現状は常にtrue。 + */ +bool GetTrendState(EAState &state) + { + bool ProcessDone = CheckDoneFile(done_trend_file); + if(ProcessDone && g_ea.load_trend_flg) + { + int trend_state; + LoadTrendState(trend_state); + state.trend_state = trend_state; + Print("market_state: ", state.trend_state, " (", MarketStateName(state.trend_state), ")"); + g_ea.load_trend_flg = false; + g_ea.last_trend_update = TimeLocal(); + } + return true; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief `trend_state.txt` からH4 market_state判定値を読み込みます。 + * + * @param trend_state 読み込んだ値を格納する参照。0..5のmarket_state。6は技術エラー停止。 + * + * ファイル未存在、読込失敗、異常値の場合は安全側として6(技術エラー停止)を設定します。 + */ +void LoadTrendState(int &trend_state) + { + string filename = trend_state_file; + trend_state = MARKET_TECHNICAL_ERROR_STOP; + + if(FileIsExist(filename)) + { + int filehandle = FileOpen(filename, FILE_READ | FILE_TXT); + if(filehandle != INVALID_HANDLE) + { + string line = FileReadString(filehandle); + int value = MARKET_TECHNICAL_ERROR_STOP; + if(TryParseStrictInteger(line, value) && + value >= MARKET_LOW_VOL_RANGE && value <= MARKET_TECHNICAL_ERROR_STOP) + trend_state = value; + else + Print("Invalid market_state value: ", line, ". Use technical error stop(6)."); + FileClose(filehandle); + } + else + { Print("Failed to open trend_state.txt"); trend_state = MARKET_TECHNICAL_ERROR_STOP; } + } + else + { Print("trend_state.txt not found"); trend_state = MARKET_TECHNICAL_ERROR_STOP; } + } + +//+------------------------------------------------------------------+ +//| ターゲット価格を取得する関数 +//+------------------------------------------------------------------+ +/** + * @brief Pythonが出力したエントリー価格群を必要なタイミングで読み込みます。 + * + * @param state EA全体の状態。`res_chk` と4タイプ分のen/tp/slを更新します。 + * @return 現状は常にtrue。 + */ +bool GetTargetPrices(EAState &state) + { + bool ProcessDone = CheckDoneFile(done_entry_file); // ←変更 + if(ProcessDone && g_ea.load_target_flg) // ←変更 + { + double target_prices[]; + LoadTargetPrices(target_prices); + + int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + + // 先頭は res_chk + state.res_chk = (int)target_prices[0]; + + // ★ 1..4 の (en,tp,sl) を読む:合計12個 + for(int t=1; t<=4; t++) + { + int base = 1 + (t-1)*3; // 1,4,7,10 + state.en_price[t] = NormalizeDouble(target_prices[base + 0], digits); + state.tp_price[t] = NormalizeDouble(target_prices[base + 1], digits); + state.sl_price[t] = NormalizeDouble(target_prices[base + 2], digits); + } + + // ログ(任意) + Print("target_prices: res=", state.res_chk, + " | T1 en=", state.en_price[1], " tp=", state.tp_price[1], " sl=", state.sl_price[1], + " | T2 en=", state.en_price[2], " tp=", state.tp_price[2], " sl=", state.sl_price[2], + " | T3 en=", state.en_price[3], " tp=", state.tp_price[3], " sl=", state.sl_price[3], + " | T4 en=", state.en_price[4], " tp=", state.tp_price[4], " sl=", state.sl_price[4]); + + LoadTargetZones(state); + if(use_split_entry_zone && cancel_old_split_pending_on_new_zone) + { + if(state.zone_res_chk == 1) + CancelStaleSplitPendingOrders(state.zone_candidate_id); + else + CancelStaleSplitPendingOrders(""); + } + + g_ea.load_target_flg = false; // ←変更 + g_ea.last_target_update = TimeLocal(); // ←変更 + g_ea.target_loaded_at = TimeCurrent(); + } + return true; + } +//+------------------------------------------------------------------+ +//+------------------------------------------------------------------+ +//| "target_prices.txt"を読み込む関数 +//+------------------------------------------------------------------+ +/** + * @brief `target_prices.txt` から13個の数値を読み込みます。 + * + * @param target_prices 読み込んだ数値を格納する配列。先頭がres_chk、以降は4タイプ分のen/tp/sl。 + * + * 行数不足の場合は`res_chk=0`として、ターゲット価格を無効扱いにします。 + */ +void LoadTargetPrices(double &target_prices[]) + { + ArrayResize(target_prices, TARGET_SIZE); + for(int i = 0; i < TARGET_SIZE; i++) + target_prices[i] = DEFAULT_TARGET_PRICE; + + string filename = target_prices_file; + + if(FileIsExist(filename)) + { + int filehandle = FileOpen(filename, FILE_READ | FILE_TXT); + if(filehandle != INVALID_HANDLE) + { + int i = 0; + bool parse_failed = false; + while(!FileIsEnding(filehandle) && i < TARGET_SIZE) + { + string line = FileReadString(filehandle); + double parsed = 0.0; + if(!TryParseStrictDouble(line, parsed)) + { + Print("target_prices.txt invalid numeric line. index=", i, " line=", line); + parse_failed = true; + break; + } + + target_prices[i] = parsed; + i++; + } + FileClose(filehandle); + + if(parse_failed || i < TARGET_SIZE) + { + if(!parse_failed) + Print("target_prices.txt line count is short. loaded=", i, " required=", TARGET_SIZE); + for(int j = 0; j < TARGET_SIZE; j++) + target_prices[j] = DEFAULT_TARGET_PRICE; + target_prices[0] = 0; // 不完全なファイルは無効扱い + } + } + else + { + Print("Failed to open file. Error code: ", GetLastError()); + } + } + else + { + Print("File does not exist: ", filename); + } + } + +//+------------------------------------------------------------------+ +//| target_zones.txt の状態を停止値へ初期化する関数 +//+------------------------------------------------------------------+ +void ResetTargetZones(EAState &state) + { + state.zone_res_chk = 0; + state.zone_candidate_id = "0"; + state.target_candidate_at = 0; + + for(int t = 1; t <= 4; t++) + { + state.zone_low[t] = 0.0; + state.zone_high[t] = 0.0; + state.zone_tp[t] = 0.0; + state.zone_sl[t] = 0.0; + } + } + +//+------------------------------------------------------------------+ +//| target_zones.txt の1戦略行を読み込む関数 +//+------------------------------------------------------------------+ +bool ParseTargetZoneLine(const string line, EAState &state, const int digits) + { + string parts[]; + int count = StringSplit(line, StringGetCharacter(",", 0), parts); + if(count < 5) + return false; + + int strategy = 0; + if(!TryParseStrictInteger(parts[0], strategy)) + return false; + if(strategy < 1 || strategy > 4) + return false; + + double zone_low = 0.0; + double zone_high = 0.0; + double zone_tp = 0.0; + double zone_sl = 0.0; + if(!TryParseStrictDouble(parts[1], zone_low) || + !TryParseStrictDouble(parts[2], zone_high) || + !TryParseStrictDouble(parts[3], zone_tp) || + !TryParseStrictDouble(parts[4], zone_sl)) + return false; + + state.zone_low[strategy] = NormalizeDouble(zone_low, digits); + state.zone_high[strategy] = NormalizeDouble(zone_high, digits); + state.zone_tp[strategy] = NormalizeDouble(zone_tp, digits); + state.zone_sl[strategy] = NormalizeDouble(zone_sl, digits); + return true; + } + +//+------------------------------------------------------------------+ +//| target_zones.txt を読み込む関数 +//+------------------------------------------------------------------+ +void LoadTargetZones(EAState &state) + { + ResetTargetZones(state); + + string filename = target_zones_file; + if(!FileIsExist(filename)) + { + Print("target_zones.txt not found. Split entry zones disabled for this candidate."); + return; + } + + int filehandle = FileOpen(filename, FILE_READ | FILE_TXT); + if(filehandle == INVALID_HANDLE) + { + Print("Failed to open target_zones.txt. Error code: ", GetLastError()); + return; + } + + int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + int line_index = 0; + int loaded_strategies = 0; + + while(!FileIsEnding(filehandle)) + { + string line = FileReadString(filehandle); + if(line == "") + continue; + + line_index++; + if(line_index == 1) + { + int schema_version = 0; + if(!TryParseStrictInteger(line, schema_version) || + schema_version != TARGET_ZONE_SCHEMA_VERSION) + { + Print("target_zones schema mismatch. loaded=", line, + " required=", TARGET_ZONE_SCHEMA_VERSION); + FileClose(filehandle); + ResetTargetZones(state); + return; + } + continue; + } + + if(line_index == 2) + { + int zone_res = 0; + if(!TryParseStrictInteger(line, zone_res)) + { + Print("target_zones invalid res_chk line: ", line); + FileClose(filehandle); + ResetTargetZones(state); + return; + } + + state.zone_res_chk = zone_res; + continue; + } + + if(line_index == 3) + { + state.zone_candidate_id = NormalizeTargetCandidateId(line); + datetime candidate_at = 0; + if(TryParseTargetCandidateTime(state.zone_candidate_id, candidate_at)) + state.target_candidate_at = candidate_at; + else + state.target_candidate_at = 0; + continue; + } + + if(ParseTargetZoneLine(line, state, digits)) + loaded_strategies++; + } + + FileClose(filehandle); + + if(line_index < 7 || loaded_strategies < 4) + { + Print("target_zones.txt line count is short. loaded_lines=", line_index, + " loaded_strategies=", loaded_strategies); + ResetTargetZones(state); + return; + } + + if(state.zone_res_chk != 1) + ResetTargetZones(state); + + Print("target_zones: res=", state.zone_res_chk, + " id=", state.zone_candidate_id, + " candidate_at=", TimeToString(state.target_candidate_at, TIME_DATE | TIME_MINUTES), + " | T1 zone=", state.zone_low[1], "-", state.zone_high[1], + " tp=", state.zone_tp[1], " sl=", state.zone_sl[1], + " | T2 zone=", state.zone_low[2], "-", state.zone_high[2], + " tp=", state.zone_tp[2], " sl=", state.zone_sl[2], + " | T3 zone=", state.zone_low[3], "-", state.zone_high[3], + " tp=", state.zone_tp[3], " sl=", state.zone_sl[3], + " | T4 zone=", state.zone_low[4], "-", state.zone_high[4], + " tp=", state.zone_tp[4], " sl=", state.zone_sl[4]); + } + +//+------------------------------------------------------------------+ + +#endif diff --git a/Include/MyLib/Trading/HITTradeManager.mqh b/Include/MyLib/Trading/HITTradeManager.mqh index 744230b..95cca47 100644 --- a/Include/MyLib/Trading/HITTradeManager.mqh +++ b/Include/MyLib/Trading/HITTradeManager.mqh @@ -73,10 +73,12 @@ int VolumeDigits() * @param tp 利確価格。 * @param sl 損切価格。 * @param volume 注文ロット。 - * @param comment_suffix 分割注文識別用のコメント接尾辞。空なら従来コメント。 + * @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) +bool SendOrder(int orderType, double price, double tp, double sl, double volume, + string comment_suffix, datetime candidate_reference_time) { MqlTradeRequest request = {}; MqlTradeResult result = {}; @@ -95,7 +97,8 @@ bool SendOrder(int orderType, double price, double tp, double sl, double volume, // 指値または逆指値注文 request.action = TRADE_ACTION_PENDING; - ApplyPendingOrderExpiration(request); + if(!ApplyPendingOrderExpiration(request, candidate_reference_time)) + return false; switch(orderType) { @@ -178,35 +181,48 @@ ENUM_ORDER_TYPE_FILLING GetOrderFillingPolicy(string symbol) * @brief brokerが対応している範囲でpending orderにサーバー側の期限を設定します。 * * @param request 更新対象の取引リクエスト。 + * @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。 + * @return H1候補の期限が残っている場合はtrue。発注前に期限切れならfalse。 */ -void ApplyPendingOrderExpiration(MqlTradeRequest &request) +bool ApplyPendingOrderExpiration(MqlTradeRequest &request, const datetime candidate_reference_time) { int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1); - long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE); + datetime expiration_at = TimeCurrent() + expiration_seconds; + if(candidate_reference_time > 0) + expiration_at = candidate_reference_time + expiration_seconds; - if(expiration_seconds > 0 && - (expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0) + if(expiration_seconds <= 0 || expiration_at <= TimeCurrent()) { - request.type_time = ORDER_TIME_SPECIFIED; - request.expiration = TimeCurrent() + expiration_seconds; - return; + 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; } - if(expiration_seconds > 0 && - (expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0) + 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 = TimeCurrent() + expiration_seconds; - return; + request.expiration = expiration_at; + return true; } if((expiration_mode & SYMBOL_EXPIRATION_DAY) != 0) { request.type_time = ORDER_TIME_DAY; - return; + return true; } request.type_time = ORDER_TIME_GTC; + return true; } //+------------------------------------------------------------------+ @@ -391,6 +407,215 @@ bool IsCurrentSplitCandidateComment(const string comment, const string candidate 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が既存注文/ポジションに存在するか判定する関数 //+------------------------------------------------------------------+ @@ -437,6 +662,7 @@ 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--) { @@ -456,26 +682,8 @@ bool CancelStaleSplitPendingOrders(const string current_candidate_id) if(keep_current_candidate && IsCurrentSplitCandidateComment(comment, current_candidate_id)) continue; - MqlTradeRequest request = {}; - MqlTradeResult trade_result = {}; - request.action = TRADE_ACTION_REMOVE; - request.order = ticket; - - if(!OrderSend(request, trade_result)) - { - Print("Failed to delete stale split order. Ticket: ", ticket, " Error: ", GetLastError()); - continue; - } - - if(trade_result.retcode == TRADE_RETCODE_DONE) - { - Print("Stale split order canceled. Ticket: ", ticket, " comment=", comment); + if(TryCancelPendingOrder(ticket, "stale split comment=" + comment)) result = true; - } - else - { - Print("Stale split order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode); - } } return result; @@ -495,6 +703,7 @@ bool CancelStaleSplitPendingOrders(const string current_candidate_id) bool CancelExpiredOrders() { bool result = false; + CleanupPendingOrderCancelAttempts(); // 未決済注文の数を取得 for(int i = OrdersTotal() - 1; i >= 0; i--) @@ -508,33 +717,18 @@ bool CancelExpiredOrders() 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(open_time <= 0 || expiration_seconds <= 0) + if(reference_time <= 0 || expiration_seconds <= 0) continue; - if(TimeCurrent() - open_time >= expiration_seconds) + if(TimeCurrent() - reference_time >= expiration_seconds) { - MqlTradeRequest request = {}; - MqlTradeResult trade_result = {}; - request.action=TRADE_ACTION_REMOVE; // 取引操作タイプ - request.order = ticket; // 注文チケット - if(!OrderSend(request, trade_result)) // 削除リクエスト送信 - { - Print("Failed to delete order. Ticket: ", ticket, " Error: ", GetLastError()); - } - else - { - if(trade_result.retcode == TRADE_RETCODE_DONE) - { - Print("Order canceled successfully due to a time limit. Ticket: ", ticket); - result = true; // 削除成功フラグ - } - else - { - Print("Order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode); - } - } + if(TryCancelPendingOrder(ticket, "H1 candidate expired")) + result = true; } } } diff --git a/Include/MyLib/Trading/HITTradeManager.mqh.bak_codex_20260531_170419 b/Include/MyLib/Trading/HITTradeManager.mqh.bak_codex_20260531_170419 new file mode 100644 index 0000000..744230b --- /dev/null +++ b/Include/MyLib/Trading/HITTradeManager.mqh.bak_codex_20260531_170419 @@ -0,0 +1,625 @@ +#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 分割注文識別用のコメント接尾辞。空なら従来コメント。 + * @return 注文が正常に受理された場合はtrue、それ以外はfalse。 + */ +bool SendOrder(int orderType, double price, double tp, double sl, double volume, string comment_suffix) + { + 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; + ApplyPendingOrderExpiration(request); + + 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 更新対象の取引リクエスト。 + */ +void ApplyPendingOrderExpiration(MqlTradeRequest &request) + { + int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1); + long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE); + + if(expiration_seconds > 0 && + (expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0) + { + request.type_time = ORDER_TIME_SPECIFIED; + request.expiration = TimeCurrent() + expiration_seconds; + return; + } + + if(expiration_seconds > 0 && + (expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0) + { + request.type_time = ORDER_TIME_SPECIFIED_DAY; + request.expiration = TimeCurrent() + expiration_seconds; + return; + } + + if((expiration_mode & SYMBOL_EXPIRATION_DAY) != 0) + { + request.type_time = ORDER_TIME_DAY; + return; + } + + request.type_time = ORDER_TIME_GTC; + } + +//+------------------------------------------------------------------+ +//| 注文コメント(ローカル時刻)を生成する関数(先頭に 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); + } + +//+------------------------------------------------------------------+ +//| 分割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"); + + 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; + + MqlTradeRequest request = {}; + MqlTradeResult trade_result = {}; + request.action = TRADE_ACTION_REMOVE; + request.order = ticket; + + if(!OrderSend(request, trade_result)) + { + Print("Failed to delete stale split order. Ticket: ", ticket, " Error: ", GetLastError()); + continue; + } + + if(trade_result.retcode == TRADE_RETCODE_DONE) + { + Print("Stale split order canceled. Ticket: ", ticket, " comment=", comment); + result = true; + } + else + { + Print("Stale split order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode); + } + } + + return result; + } + + +//+------------------------------------------------------------------+ +//| 時間が経過したエントリー注文をキャンセルする関数 +//+------------------------------------------------------------------+ +/** + * @brief 実時間でENTRY_H1_LIMIT時間を超えた未約定注文をキャンセルします。 + * + * @return 1件以上キャンセルに成功した場合はtrue。 + * + * 対象は`_Symbol` と `magic_number` が一致する未約定注文のみです。 + */ +bool CancelExpiredOrders() + { + bool result = false; + +// 未決済注文の数を取得 + 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); + + int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1); + if(open_time <= 0 || expiration_seconds <= 0) + continue; + + if(TimeCurrent() - open_time >= expiration_seconds) + { + MqlTradeRequest request = {}; + MqlTradeResult trade_result = {}; + request.action=TRADE_ACTION_REMOVE; // 取引操作タイプ + request.order = ticket; // 注文チケット + if(!OrderSend(request, trade_result)) // 削除リクエスト送信 + { + Print("Failed to delete order. Ticket: ", ticket, " Error: ", GetLastError()); + } + else + { + if(trade_result.retcode == TRADE_RETCODE_DONE) + { + Print("Order canceled successfully due to a time limit. Ticket: ", ticket); + result = true; // 削除成功フラグ + } + else + { + Print("Order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode); + } + } + } + } + } + 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 diff --git a/docs/MyProject/HIT-EA_spec_en.md b/docs/MyProject/HIT-EA_spec_en.md index d119b59..b9f2b1c 100644 --- a/docs/MyProject/HIT-EA_spec_en.md +++ b/docs/MyProject/HIT-EA_spec_en.md @@ -20,7 +20,7 @@ - `lot_size = 0.01`: Order volume. - `spread_limit = 60`: Maximum allowed spread in points. - `magic_number = 10001`: Magic number used to identify this EA's orders and positions. -- `initial_order = 0`: Enables initial H4/H1 processing on startup when set to `1`. +- `initial_order = 0`: Retained for compatibility with older settings. Startup always rebuilds H4/H1 results and remains in technical-error stop state until the H4 result is reloaded. - `input_position_limit = 10`: Practical limit for this EA's pending orders plus positions. Values above the internal `POSITION_LIMIT` are clamped. - `use_split_entry_zone = false`: Enables split entries based on H1 predicted zones. - `split_entry_count = 3`: Number of split entry orders, clamped to 1..10. @@ -28,6 +28,7 @@ - `split_total_lot_size = 0.09`: Total lot used by total-split mode. - `split_fixed_lot_size = 0.01`: Per-order lot used by fixed-split mode. - `cancel_old_split_pending_on_new_zone = true`: Cancels older split pending orders when a new H1 zone set is loaded. +- `input_cancel_retry_cooldown_seconds = 60`: Wait time before retrying a failed pending-order cancellation for the same ticket. The effective value is clamped to 1..3600 seconds. - `use_m15_entry_filter = true`: Enables M15 closed-bar entry timing confirmation. - `m15_entry_zone_atr_multiplier = 1.50`: Allowed proximity multiplier based on the M15 average range. - `use_m15_imbalance_confirmation = true`: Adds M15 impulse confirmation for T1/T3 trend-following candidates. @@ -148,7 +149,7 @@ New entries are allowed only when all conditions below are satisfied: - The order-type price relationship and broker minimum distance rules are satisfied. - When `use_m15_entry_filter = true`, T2/T4 satisfy the M15 direction, rejection, and proximity checks. T1/T3 satisfy M15 impulse confirmation when `use_m15_imbalance_confirmation = true`. - This EA's pending orders plus positions are below `input_position_limit`. -- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + slot. +- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + order type + slot. ### market_state and Allowed Order Types @@ -160,6 +161,8 @@ New entries are allowed only when all conditions below are satisfied: - `5 MARKET_HIGH_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit. - `6 MARKET_TECHNICAL_ERROR_STOP`: Blocks new entries. +After an H4 `market_state` reload, the EA cancels existing pending orders whose order types are not allowed by the refreshed state. For `6 MARKET_TECHNICAL_ERROR_STOP`, it cancels all pending orders managed by this EA. + ### Order-Type Price Rules - T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry` @@ -169,7 +172,7 @@ New entries are allowed only when all conditions below are satisfied: ## 7. Exit Conditions -- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from setup time. +- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from the H1 candidate time restored from the comment. Legacy orders without that token fall back to setup time. - Positions are closed after `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from entry time. - Time-limit handling runs early in `OnTick` and is independent of Python readiness or spread checks. - On EA removal, `OnDeinit()` releases external-process state and the SLTP panel, then clears the chart status text with `Comment("")` and `ChartRedraw(0)`. @@ -191,10 +194,13 @@ New entries are allowed only when all conditions below are satisfied: - `input_position_limit` counts only this EA's orders and positions, not the full account. The hard internal limit is `POSITION_LIMIT`. - The EA selects the filling policy from `SYMBOL_FILLING_MODE`, preferring IOC, then FOK, then RETURN. - Before sending either single or split orders, the EA verifies the requested lot against `SYMBOL_VOLUME_MIN/MAX/STEP`. -- Pending orders receive server-side expiration when the broker supports it. +- Pending orders receive server-side expiration from the H1 candidate time when the broker supports it. Orders are skipped if that expiration is already in the past. - `OrderSend` return values and `MqlTradeResult.retcode` are checked. Failures are logged with `GetLastError()` or the retcode. -- Split-entry order comments include `candidate_id` and slot number to prevent duplicate slot execution. +- Single and split order comments keep a valid `candidate_id` for candidate-origin expiration handling. +- Split-entry order comments also include order type and slot number to prevent duplicate execution of the same strategy slot. - When `cancel_old_split_pending_on_new_zone = true`, a new valid `candidate_id` cancels older split pending orders, and an invalid or stopped `target_zones.txt` cancels all existing split pending orders for this EA/symbol/magic. +- After an H4 state update, existing pending orders that contradict the refreshed `market_state` are cancelled. +- A failed pending-order cancellation is not retried for the same ticket until `input_cancel_retry_cooldown_seconds` has elapsed, preventing excessive per-tick trade-server requests. - SLTP management only targets existing positions matching `_Symbol` and `magic_number`, and it preserves each position's current TP. - SL changes are sent only when the candidate improves protection relative to the current SL and satisfies broker stop-level and freeze-level constraints. - `HighVolatilityLimit()` evaluates BUY positions with Bid and SELL positions with Ask, and treats GOLD/XAUUSD suffix symbols as 1 pip = 0.1. @@ -203,13 +209,22 @@ New entries are allowed only when all conditions below are satisfied: - If `trend_state.txt` is missing, unreadable, not strictly parseable as an integer, or outside `0..6`, the EA uses `MARKET_TECHNICAL_ERROR_STOP`. - If `target_prices.txt` is missing, unreadable, shorter than 13 lines, or contains a non-strict numeric line, the EA uses `res_chk = 0`. -- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, or non-strict numeric values stop new split orders. +- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, a valid-zone `candidate_id` that is not a strict 12-digit timestamp, or non-strict numeric values stop new split orders. - If a Python process exits with a non-zero code, the code is logged and the next trigger may retry. - If a Python process remains active beyond `PYTHON_TIMEOUT_SECONDS`, the EA runs `taskkill /T /F` against the cmd/bat process tree, clears done/running files, and allows the next trigger to retry. - If only a stale running marker remains, the EA attempts PID recovery. If recovery fails and the marker is timed out, it removes the stale marker. ## 10. Changelog +### 2026-05-31 + +- Changed startup initialization to rebuild H4/H1 results regardless of `initial_order`, while holding the technical-error stop state until the H4 result reloads. +- Added cancellation of existing pending orders that contradict a refreshed H4 `market_state`. +- Added per-ticket cooldown after failed pending-order cancellation to prevent excessive per-tick trade-server requests. +- Added order type to split-slot identifiers so different strategies using the same `candidate_id` and slot cannot collide. +- Kept valid `candidate_id` tokens in both single and split order comments and changed pending expiration to use the H1 candidate origin time. +- Required strict 12-digit timestamp `candidate_id` values for valid `target_zones.txt`; malformed values now stop safely. + ### 2026-05-30 - Added the `HIT__` linkage-file prefix contract on both the MT5 and Python sides to avoid collisions across multiple EAs or symbols in the same terminal. diff --git a/docs/MyProject/HIT-EA_spec_en.md.bak_codex_20260531_170419 b/docs/MyProject/HIT-EA_spec_en.md.bak_codex_20260531_170419 new file mode 100644 index 0000000..d119b59 --- /dev/null +++ b/docs/MyProject/HIT-EA_spec_en.md.bak_codex_20260531_170419 @@ -0,0 +1,259 @@ +# HIT-EA Specification (English) + +## 1. Overview + +- This EA sends confirmed H4 OHLC data to an external Python process for market-state analysis and confirmed H1 OHLC data for entry candidate generation. +- The MQL5 side reads Python output files and places pending orders only after checking the H4 market state, H1 candidate prices, M15 confirmation filter, spread, and broker distance constraints. +- For backtest reproducibility, OHLC export uses `OHLC_START_SHIFT = 1`, so the forming bar is excluded and only closed bars are passed to Python. +- For live resilience, the EA tracks external-process running files, process IDs, exit codes, and timeouts to prevent duplicate launches and CSV overwrites. + +## 2. Indicators + +- No standard or custom indicators are used. +- OHLC data is retrieved with `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)`. +- M15 entry confirmation uses candle shape, average range, and distance to the candidate price for T2/T4. For T1/T3 it uses M15 impulse confirmation when that option is enabled. + +## 3. Parameters + +### input + +- `lot_size = 0.01`: Order volume. +- `spread_limit = 60`: Maximum allowed spread in points. +- `magic_number = 10001`: Magic number used to identify this EA's orders and positions. +- `initial_order = 0`: Enables initial H4/H1 processing on startup when set to `1`. +- `input_position_limit = 10`: Practical limit for this EA's pending orders plus positions. Values above the internal `POSITION_LIMIT` are clamped. +- `use_split_entry_zone = false`: Enables split entries based on H1 predicted zones. +- `split_entry_count = 3`: Number of split entry orders, clamped to 1..10. +- `split_lot_mode = SPLIT_LOT_TOTAL`: Split lot mode. `SPLIT_LOT_TOTAL` divides the total lot across slots, and `SPLIT_LOT_FIXED` uses a fixed lot per order. +- `split_total_lot_size = 0.09`: Total lot used by total-split mode. +- `split_fixed_lot_size = 0.01`: Per-order lot used by fixed-split mode. +- `cancel_old_split_pending_on_new_zone = true`: Cancels older split pending orders when a new H1 zone set is loaded. +- `use_m15_entry_filter = true`: Enables M15 closed-bar entry timing confirmation. +- `m15_entry_zone_atr_multiplier = 1.50`: Allowed proximity multiplier based on the M15 average range. +- `use_m15_imbalance_confirmation = true`: Adds M15 impulse confirmation for T1/T3 trend-following candidates. +- `m15_imbalance_avg_body_period = 20`: Average-body period used by M15 impulse confirmation. +- `m15_imbalance_sensitivity = 2.0`: Required multiple of the average body for M15 impulse confirmation. +- `m15_imbalance_min_avg_body_points = 1.0`: Minimum M15 average body in points. +- `use_m15_imbalance_debug_log = false`: Enables detailed M15 impulse-confirmation logs. +- `input_entry_max_candidate_age_minutes = 120`: Maximum age, in minutes, for using an H1 candidate for new execution. This blocks delayed entries when M15 confirmation appears too late. +- `input_sltp_manager_enabled = false`: Enables UI-driven existing-position SL management through `CSLTPManager`. +- `input_sltp_show_panel = true`: Shows the SLTP control panel on the chart. +- `input_sltp_use_breakeven = false`: Enables normal break-even. +- `input_sltp_breakeven_trigger_pips = 30.0`: Profit threshold for normal break-even. +- `input_sltp_breakeven_buffer_pips = 3.0`: Profit-side entry buffer used by normal break-even. +- `input_sltp_use_elapsed_breakeven = false`: Enables elapsed-time break-even. +- `input_sltp_elapsed_breakeven_hours = 4.0`: Holding time required before elapsed-time break-even is allowed. +- `input_sltp_elapsed_breakeven_buffer_pips = 3.0`: Profit-side entry buffer used by elapsed-time break-even. +- `input_sltp_use_active_trailing = false`: Enables active step trailing. +- `input_sltp_active_breakeven_pips = 30.0`: Profit threshold that starts active trailing. +- `input_sltp_active_stop_loss_offset_pips = 5.0`: Initial locked profit offset from entry. +- `input_sltp_active_step_trigger_pips = 10.0`: Additional profit interval required for each SL step. +- `input_sltp_active_step_move_pips = 5.0`: SL movement added per completed step. +- `input_sltp_use_tp_progress_stop = false`: Enables TP-progress-based SL locking. +- `input_sltp_tp_progress_trigger_percent = 70.0`: TP progress required before the SL lock is applied. +- `input_sltp_tp_progress_sl_lock_percent = 30.0`: Entry-to-TP distance percentage locked by SL. +- `input_sltp_use_high_volatility_limit = false`: Enables short-term high-volatility SL tightening. + +### Main Constants + +- `POSITION_LIMIT = 48`: Maximum number of this EA's pending orders plus positions. +- `ENTRY_H1_LIMIT = 2`: Pending-order lifetime in H1 bars. +- `CLOSE_H1_LIMIT = 12`: Position lifetime in H1 bars. +- `HISTORY_BARS = 72`: Number of OHLC bars exported to Python. +- `OHLC_START_SHIFT = 1`: Shift used to export closed bars only. +- `M15_CONFIRM_BARS = 30`: Number of M15 bars used for average-range calculation. +- `ENTRY_RETRY_SECONDS = 60`: Retry interval for entry checks. +- `ENTRY_RETRY_LIMIT = 10`: Maximum retry count for one H1 candidate set. +- `TARGET_SIZE = 13`: Number of numeric values expected in `target_prices.txt`. +- `TARGET_ZONE_SCHEMA_VERSION = 2`: Schema version used by `target_zones.txt`. +- `PYTHON_TIMEOUT_SECONDS = 600`: External Python wait threshold. + +### Python Entry-Candidate Guard + +- After H1 candidate generation, Python invalidates any `entry` that is too far from the current price. +- The maximum distance is strategy-specific: T1/T3 Stop candidates use `max(H1 EATR * 1.50, 5.00)`, and T2/T4 Limit candidates use `max(H1 EATR * 1.00, 5.00)`. A strategy beyond its limit is rewritten to `0.00,0.00,0.00`. +- This post-filter is intended to reduce delayed entries from deep limit orders or distant breakout waits after M15 confirmation arrives late. + +## 4. File Layout + +- `Experts/MyProject/HIT-EA_refactor_ver6.mq5`: EA entry point. Keeps `OnInit`, `OnDeinit`, `OnTick`, inputs, global state, and DLL imports. +- `Include/MyLib/Common/HITRuntimeController.mqh`: Tick flow, H4/H1/M15 update control, status comments, and spread checks. +- `Include/MyLib/Signals/HITEntrySignal.mqh`: Entry preconditions, order-type price validation, M15 filter, and retry-state updates. +- `Include/MyLib/Common/HITExternalProcess.mqh`: done/running files, external process handles, PID recovery, exit-code checks, and timeout recovery. +- `Include/MyLib/Signals/HITPythonSignalGateway.mqh`: OHLC CSV export, Python batch launch, and `trend_state.txt` / `target_prices.txt` / `target_zones.txt` loading. +- `Include/MyLib/Trading/HITTradeManager.mqh`: Order sending, filling policy, pending-order expiration, order/position counting, expired order cancellation, and expired position closing. +- `Include/MyLib/Trading/SLTPManager.mqh`: Existing-position break-even, elapsed-time break-even, active trailing, TP-progress SL, and high-volatility SL management. +- `Include/MyLib/Panel/SLTPManagerPanel.mqh`: Chart panel that toggles `CSLTPManager` features, validates numeric inputs, and applies settings to the EA. + +## 5. Python Integration Files + +During `OnInit()`, the EA builds a `HIT__` prefix from `_Symbol` and `magic_number`, then applies it to all MT5 `Files` linkage names. Example: `HIT_GOLD_10001_ohlc_H4.csv`. The same prefix is passed to the batch file as the first argument, and the batch file exposes it to Python as `MT5_EA_FILE_PREFIX`. The EA also passes the symbol's `SYMBOL_DIGITS` as the second argument, exposed to Python as `MT5_PRICE_DIGITS`. + +When no prefix is provided, such as manual Python execution, Python keeps using the legacy names like `ohlc_H4.csv`. + +### H4 Trend Analysis + +- MQL5 output: `_ohlc_H4.csv` +- Python output: `_trend_state.txt` +- Done marker: `_process_done_trend.txt` +- Running marker: `_process_running_trend.txt` +- Batch file: `\MQL5\python_for_ea\bat\get_trend_reply.bat` + +### H1 Entry Candidate Generation + +- MQL5 output: `_ohlc_H1.csv` +- Python output: `_target_prices.txt`, `_target_zones.txt` +- Done marker: `_process_done_entry.txt` +- Running marker: `_process_running_entry.txt` +- Batch file: `\MQL5\python_for_ea\bat\get_entry_reply.bat` + +During `OnInit()`, the EA resolves `MQL5\python_for_ea` from `TerminalInfoString(TERMINAL_DATA_PATH)` and builds absolute batch-file paths. Each batch file resolves the Python project root from its own location, so runtime execution no longer depends on `C:\ea_py`. + +`target_prices.txt` must contain 13 lines: + +```text +Line 1: res_chk +Lines 2-4: T1 Buy Stop entry/tp/sl +Lines 5-7: T2 Buy Limit entry/tp/sl +Lines 8-10: T3 Sell Stop entry/tp/sl +Lines 11-13: T4 Sell Limit entry/tp/sl +``` + +`target_zones.txt` must contain 7 lines for split entries: + +```text +Line 1: schema_version (2) +Line 2: res_chk +Line 3: candidate_id derived from the closed H1 bar time +Line 4: T1 Buy Stop strategy, zone_low, zone_high, tp, sl +Line 5: T2 Buy Limit strategy, zone_low, zone_high, tp, sl +Line 6: T3 Sell Stop strategy, zone_low, zone_high, tp, sl +Line 7: T4 Sell Limit strategy, zone_low, zone_high, tp, sl +``` + +Python creates `process_done_entry.txt` only after both `target_prices.txt` and `target_zones.txt` have been written. Prices in `target_zones.txt` are formatted with `MT5_PRICE_DIGITS`; manual runs without this value use a conservative 5 decimal places. When split entries are disabled, the EA keeps using the legacy 13-line `target_prices.txt` path. + +## 6. Entry Conditions + +New entries are allowed only when all conditions below are satisfied: + +- Spread is less than or equal to `spread_limit`. +- H4 trend analysis is complete and `trend_state.txt` can be loaded. +- H1 entry candidate generation is complete and `target_prices.txt` can be loaded. +- `res_chk = 1`. +- The H1 candidate set has not expired under `ENTRY_H1_LIMIT`, measured from the closed H1 bar time that produced the candidate. +- The H1 candidate set has not exceeded `input_entry_max_candidate_age_minutes`, measured from the closed H1 bar time that produced the candidate. +- The selected order type has valid `entry/tp/sl` values greater than `0.0`. +- The order type is allowed by the H4 `market_state`. +- The order-type price relationship and broker minimum distance rules are satisfied. +- When `use_m15_entry_filter = true`, T2/T4 satisfy the M15 direction, rejection, and proximity checks. T1/T3 satisfy M15 impulse confirmation when `use_m15_imbalance_confirmation = true`. +- This EA's pending orders plus positions are below `input_position_limit`. +- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + slot. + +### market_state and Allowed Order Types + +- `0 MARKET_LOW_VOL_RANGE`: Allows T2 Buy Limit / T4 Sell Limit. +- `1 MARKET_HIGH_VOL_RANGE`: Allows T2 Buy Limit / T4 Sell Limit. +- `2 MARKET_LOW_VOL_UP`: Allows T1 Buy Stop / T2 Buy Limit. +- `3 MARKET_HIGH_VOL_UP`: Allows T1 Buy Stop / T2 Buy Limit. +- `4 MARKET_LOW_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit. +- `5 MARKET_HIGH_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit. +- `6 MARKET_TECHNICAL_ERROR_STOP`: Blocks new entries. + +### Order-Type Price Rules + +- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry` +- T2 Buy Limit: `Ask > entry`, `tp > entry`, `sl < entry` +- T3 Sell Stop: `Bid > entry`, `tp < entry`, `sl > entry` +- T4 Sell Limit: `Bid < entry`, `tp < entry`, `sl > entry` + +## 7. Exit Conditions + +- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from setup time. +- Positions are closed after `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from entry time. +- Time-limit handling runs early in `OnTick` and is independent of Python readiness or spread checks. +- On EA removal, `OnDeinit()` releases external-process state and the SLTP panel, then clears the chart status text with `Comment("")` and `ChartRedraw(0)`. +- When `input_sltp_manager_enabled` or the panel `Manager` toggle is ON, the EA runs `CSLTPManager::ManagePositions()` and `HighVolatilityLimit()` after time-limit handling. Existing-position SL protection therefore continues while Python results are pending or new entries are skipped. +- Normal break-even and active trailing are mutually exclusive. The panel turns one mode off when the other is enabled, and `CSLTPManager::ValidateSettings()` also validates the exclusion. + +## 7.1 SLTP Control Panel + +- The panel is created when `input_sltp_show_panel = true` and appears near the chart's upper-right area. +- If panel creation fails, the EA logs the failure and continues with the already-applied input-based SLTP settings. +- The `Manager` button toggles all SLTP management. When OFF, panel values remain stored but `OnTick` does not modify SL values. +- The `BreakEven`, `Elapsed BE`, `Active Trail`, `TP Progress`, and `High Vol` buttons toggle each `CSLTPManager` feature. +- Numeric fields are validated when `APPLY` is pressed. Invalid or out-of-range values show a `MessageBox`, are logged, and revert to the previous valid value. +- Settings are copied into `CSLTPManager` only after `APPLY` succeeds. The EA then calls `ValidateSettings()`; if validation fails, SLTP management is stopped and the reason is logged. + +## 8. Risk Management + +- Managed orders and positions are limited to matching `_Symbol` and `magic_number`. +- `input_position_limit` counts only this EA's orders and positions, not the full account. The hard internal limit is `POSITION_LIMIT`. +- The EA selects the filling policy from `SYMBOL_FILLING_MODE`, preferring IOC, then FOK, then RETURN. +- Before sending either single or split orders, the EA verifies the requested lot against `SYMBOL_VOLUME_MIN/MAX/STEP`. +- Pending orders receive server-side expiration when the broker supports it. +- `OrderSend` return values and `MqlTradeResult.retcode` are checked. Failures are logged with `GetLastError()` or the retcode. +- Split-entry order comments include `candidate_id` and slot number to prevent duplicate slot execution. +- When `cancel_old_split_pending_on_new_zone = true`, a new valid `candidate_id` cancels older split pending orders, and an invalid or stopped `target_zones.txt` cancels all existing split pending orders for this EA/symbol/magic. +- SLTP management only targets existing positions matching `_Symbol` and `magic_number`, and it preserves each position's current TP. +- SL changes are sent only when the candidate improves protection relative to the current SL and satisfies broker stop-level and freeze-level constraints. +- `HighVolatilityLimit()` evaluates BUY positions with Bid and SELL positions with Ask, and treats GOLD/XAUUSD suffix symbols as 1 pip = 0.1. + +## 9. Error Handling + +- If `trend_state.txt` is missing, unreadable, not strictly parseable as an integer, or outside `0..6`, the EA uses `MARKET_TECHNICAL_ERROR_STOP`. +- If `target_prices.txt` is missing, unreadable, shorter than 13 lines, or contains a non-strict numeric line, the EA uses `res_chk = 0`. +- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, or non-strict numeric values stop new split orders. +- If a Python process exits with a non-zero code, the code is logged and the next trigger may retry. +- If a Python process remains active beyond `PYTHON_TIMEOUT_SECONDS`, the EA runs `taskkill /T /F` against the cmd/bat process tree, clears done/running files, and allows the next trigger to retry. +- If only a stale running marker remains, the EA attempts PID recovery. If recovery fails and the marker is timed out, it removes the stale marker. + +## 10. Changelog + +### 2026-05-30 + +- Added the `HIT__` linkage-file prefix contract on both the MT5 and Python sides to avoid collisions across multiple EAs or symbols in the same terminal. +- Updated T1/T3 trend-following candidates so `use_m15_imbalance_confirmation = true` requires M15 impulse confirmation. +- Added strict numeric parsing requirements for `trend_state.txt`, `target_prices.txt`, and `target_zones.txt`, with invalid content handled as a safe stop state. +- Added stale split-pending cancellation behavior when `target_zones.txt` is invalid or stopped. +- Synchronized current defaults for `input_entry_max_candidate_age_minutes`, `ENTRY_H1_LIMIT`, SLTP buffers, and the Python entry-distance guard. +- Added process-tree termination and retry recovery for Python timeouts. +- Changed H1 candidate freshness checks to use the closed H1 bar time restored from `candidate_id`, instead of the Python result load time. +- Added `MT5_PRICE_DIGITS` so Python formats `target_zones.txt` prices with the active symbol's decimal precision. +- Added single-order lot validation, OHLC CSV NaN/inf and consistency checks, Ask-based SELL high-volatility SL evaluation, and GOLD/XAUUSD suffix handling. + +### 2026-05-27 + +- Moved the Python helper application specification from `C:\ea_py` to `MQL5\python_for_ea` under the MT5 data folder. +- Updated the EA startup path model so batch paths are built from `TerminalInfoString(TERMINAL_DATA_PATH)`, and the external process working directory is derived from the batch-file location. +- Updated Python path discovery so the default MT5 linkage is the adjacent `MQL5\Files` directory, with `MT5_FILES_DIR` or `MT5_DATA_PATH` available for explicit overrides. + +### 2026-05-24 + +- Added Python-side `target_zones.txt` output and made `process_done_entry.txt` appear only after both `target_prices.txt` and `target_zones.txt` are written. +- Added `use_split_entry_zone`, `split_entry_count`, `split_lot_mode`, `split_total_lot_size`, `split_fixed_lot_size`, and `input_position_limit`. +- Added split-zone pending order placement with total-lot or fixed-lot sizing. +- Added `candidate_id` + slot identification to prevent duplicate split orders. + +### 2026-05-13 + +- Added a Python-side H1 EATR distance guard that invalidates candidates too far from the current price before they are passed to MT5. +- Added `input_entry_max_candidate_age_minutes` on the EA side so stale H1 candidates are not executed when M15 confirmation arrives late. +- Added H1 candidate age to entry skip logs to make delayed entries, M15 rejection, and price mismatch easier to diagnose. + +### 2026-05-05 + +- Added `Comment("")` and `ChartRedraw(0)` to `OnDeinit()` so the chart status comment is removed when the EA is deleted. +- Connected `CSLTPManager` and the dedicated chart panel `SLTPManagerPanel.mqh` to `HIT-EA_refactor_ver5.mq5`, allowing UI control of break-even, elapsed-time break-even, active trailing, TP-progress SL, and high-volatility SL tightening. +- Added SLTP management immediately after time-limit handling in `OnTick`, so existing-position protection continues independently of Python readiness and spread checks. +- Added SLTP panel inputs, mutual exclusion for normal break-even versus active trailing, and numeric validation on `APPLY`. +- Split the function implementations from `HIT-EA_refactor_ver5.mq5` into purpose-specific `.mqh` files. +- Kept `OnInit`, `OnDeinit`, `OnTick`, inputs, and global state in the EA source while moving behavior functions under `Include/MyLib/`. +- Updated the specification to match the current `CreateProcessW` Python process tracking, M15 filter, `market_state` range `0..6`, and `spread_limit = 60`. + +### 2026-05-02 + +- Added entry suppression when `res_chk = 0`. +- Moved time-limit handling before Python readiness and spread checks. +- Split startup flags for H4 and H1 initial processing. +- Switched OHLC retrieval to `CopyRates` and exported closed bars only. diff --git a/docs/MyProject/HIT-EA_spec_ja.md b/docs/MyProject/HIT-EA_spec_ja.md index 59a5d65..1d450ed 100644 --- a/docs/MyProject/HIT-EA_spec_ja.md +++ b/docs/MyProject/HIT-EA_spec_ja.md @@ -20,7 +20,7 @@ - `lot_size = 0.01`: 注文ロット。 - `spread_limit = 60`: 許容スプレッド(point)。 - `magic_number = 10001`: EA識別用マジックナンバー。 -- `initial_order = 0`: 起動直後の初回H4/H1処理を実行するか。`1` で有効。 +- `initial_order = 0`: 旧設定ファイルとの互換性維持用。値にかかわらず起動時はH4/H1を再生成し、H4再読込までは技術エラー停止状態を維持する。 - `use_m15_entry_filter = true`: M15確定足によるエントリータイミング確認を使用するか。 - `m15_entry_zone_atr_multiplier = 1.50`: M15平均レンジに対する候補価格接近許容倍率。 - `use_m15_imbalance_confirmation = true`: T1/T3の順張り候補に対して、M15確定足の初動確認を追加するか。 @@ -36,6 +36,7 @@ - `split_total_lot_size = 0.09`: 総量分割モードの合計ロット。 - `split_fixed_lot_size = 0.01`: 固定ロットモードの1注文ロット。 - `cancel_old_split_pending_on_new_zone = true`: 新しいH1ゾーン読込時に、同じEAの古い分割pending注文を取消するか。 +- `input_cancel_retry_cooldown_seconds = 60`: pending注文の取消に失敗した場合、同一ticketへ取消要求を再送するまでの待機秒数。1〜3600秒に丸めて使用する。 - `input_sltp_manager_enabled = false`: UI連動の `CSLTPManager` による既存ポジションSL管理を有効化するか。 - `input_sltp_show_panel = true`: チャート上にSLTP操作パネルを表示するか。 - `input_sltp_use_breakeven = false`: 通常ブレークイーブンを有効化するか。 @@ -148,7 +149,7 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて - 注文タイプごとの価格整合条件とブローカー最小距離制約を満たす。 - `use_m15_entry_filter = true` の場合、T2/T4はM15確定足の方向・反発・接近条件を満たす。T1/T3は `use_m15_imbalance_confirmation = true` の場合にM15初動確認を満たす。 - 自EAの注文 + ポジション数が `input_position_limit` 未満。 -- 分割エントリー有効時は `target_zones.txt` の `res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + slot の重複注文/ポジション不存在を満たす。 +- 分割エントリー有効時は `target_zones.txt` の `res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + 注文タイプ + slot の重複注文/ポジション不存在を満たす。 ### market_state と注文タイプ @@ -160,6 +161,8 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて - `5 MARKET_HIGH_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。 - `6 MARKET_TECHNICAL_ERROR_STOP`: 新規注文を停止。 +H4 `market_state` の再読込後は、更新後の状態で許可されない注文タイプの既存pending注文を取消する。`6 MARKET_TECHNICAL_ERROR_STOP` の場合は対象EAのpending注文をすべて取消する。 + ### 注文タイプごとの価格整合条件 - T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry` @@ -169,7 +172,7 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて ## 7. エグジット条件 -- 未約定注文は、注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。 +- 未約定注文は、コメントからH1候補時刻を復元できる場合はその時刻から、復元できない旧注文は注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。 - 保有ポジションは、建玉時刻から `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合にクローズする。 - 時間制限処理は `OnTick` の早い段階で実行し、Python完了待ちやスプレッド判定に依存させない。 - EA削除時は `OnDeinit()` で外部プロセス状態とSLTPパネルを解放し、`Comment("")` と `ChartRedraw(0)` によりチャート上のステータス表示を消去する。 @@ -191,10 +194,13 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて - `input_position_limit` は口座全体ではなく、自EA対象分のみを数える。内部の絶対上限は `POSITION_LIMIT` とする。 - 送信時は `SYMBOL_FILLING_MODE` を参照し、IOC、FOK、RETURNの順で利用可能な執行ポリシーを選択する。 - 通常注文と分割注文の送信前に、ロットが `SYMBOL_VOLUME_MIN/MAX/STEP` を満たすことを検証する。 -- ペンディング注文には、ブローカーが対応する場合にサーバー側の期限を設定する。 +- ペンディング注文には、ブローカーが対応する場合にH1候補時刻を起点とするサーバー側の期限を設定する。発注時点で期限切れなら送信しない。 - `OrderSend` の戻り値と `MqlTradeResult.retcode` を確認し、失敗時は `GetLastError()` またはretcodeをログへ出力する。 -- 分割エントリーの注文コメントには `candidate_id` とslot番号を入れ、同一slotの二重発注を抑止する。 +- 通常注文と分割注文のコメントには有効な `candidate_id` を入れ、候補時刻起点の期限管理に使用する。 +- 分割エントリーの注文コメントには注文タイプとslot番号も入れ、同一戦略slotの二重発注を抑止する。 - `cancel_old_split_pending_on_new_zone = true` の場合、新しい有効 `candidate_id` を読んだ時は旧candidateの分割pending注文を取消し、`target_zones.txt` が無効または停止値の場合は既存の分割pending注文をすべて取消する。 +- H4状態更新後は、更新後の `market_state` と矛盾する既存pending注文を取消する。 +- pending取消に失敗したticketは `input_cancel_retry_cooldown_seconds` の間再送せず、毎tickの過剰な取引サーバー要求を防止する。 - SLTP管理は `_Symbol` と `magic_number` が一致する既存ポジションのみを対象とし、TPは既存値を維持する。 - SL変更は既存SLより利益保護方向へ改善する場合だけ実行し、ブローカーのstop level / freeze levelを満たさない候補は送信しない。 - `HighVolatilityLimit()` はBUYをBid、SELLをAsk基準で評価し、GOLD/XAUUSDはサフィックス付きシンボルでも1 pip = 0.1として扱う。 @@ -203,13 +209,22 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて - `trend_state.txt` が存在しない、読み込めない、整数として厳格にパースできない、または `0..6` 以外の場合は `MARKET_TECHNICAL_ERROR_STOP` として扱う。 - `target_prices.txt` が存在しない、読み込めない、13行未満、またはいずれかの行が数値として厳格にパースできない場合は `res_chk = 0` として扱う。 -- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。 +- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`、有効ゾーンの `candidate_id` が厳格な12桁時刻ではない、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。 - Pythonプロセスが異常終了した場合、終了コードをログへ出力し、次回トリガーで再実行できる状態に戻す。 - Pythonプロセスが `PYTHON_TIMEOUT_SECONDS` を超えても実行中の場合、`taskkill /T /F` でcmd/bat配下のプロセスツリーを終了し、done/runningファイルを整理して次回トリガーで再実行できる状態に戻す。 - `running` ファイルだけが残っている場合は、PIDからプロセス復元を試みる。復元できずタイムアウト済みなら古いマーカーとして削除する。 ## 10. 変更履歴 +### 2026-05-31 + +- 起動時は `initial_order` の値にかかわらずH4/H1結果を再生成し、H4再読込までは技術エラー停止値を維持するfail-closed初期化へ変更した。 +- H4 `market_state` 更新後に、更新状態と矛盾する既存pending注文を取消する仕様を追加した。 +- pending取消失敗時にticket単位のクールダウンを設け、毎tickの取消再送による取引サーバーへの過剰リクエストを抑止した。 +- 分割slot識別子へ注文タイプを追加し、同一 `candidate_id` の異なる戦略がslotを共有しても衝突しないようにした。 +- 通常注文と分割注文のコメントへ有効な `candidate_id` を保持し、pending期限をH1候補時刻起点で管理するようにした。 +- 有効な `target_zones.txt` の `candidate_id` は厳格な12桁数字時刻を要求し、不正値は安全側で停止するようにした。 + ### 2026-05-30 - 同一Terminal内の複数EA/複数シンボルでPython連携ファイルが衝突しないよう、`HIT__` 接頭辞をMT5/Python双方のファイル名へ適用する仕様に更新した。 diff --git a/docs/MyProject/HIT-EA_spec_ja.md.bak_codex_20260531_170419 b/docs/MyProject/HIT-EA_spec_ja.md.bak_codex_20260531_170419 new file mode 100644 index 0000000..59a5d65 --- /dev/null +++ b/docs/MyProject/HIT-EA_spec_ja.md.bak_codex_20260531_170419 @@ -0,0 +1,259 @@ +# HIT-EA 仕様書(日本語) + +## 1. 概要 + +- 本EAは、H4の確定足OHLCから外部Pythonで相場状態を判定し、H1の確定足OHLCからエントリー候補価格を生成する。 +- MQL5側はPythonの出力ファイルを読み込み、H4相場状態、H1候補価格、M15確定足フィルタ、スプレッド、ブローカー距離制約を確認したうえでペンディング注文を発行する。 +- バックテスト再現性のため、Pythonへ渡すOHLCは `OHLC_START_SHIFT = 1` により形成中バーを除外し、確定足だけを使用する。 +- ライブ実行時の耐性として、外部プロセスの `running` ファイル、プロセスID、終了コード、タイムアウトを監視し、二重起動とCSV上書きを抑止する。 + +## 2. 使用インジケータ + +- 標準インジケータおよびカスタムインジケータは未使用。 +- OHLCは `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)` で取得する。 +- M15エントリー確認は、T2/T4では直近確定足と1本前の確定足のローソク足形状、平均レンジ、候補価格との距離から判定し、T1/T3では必要に応じてM15初動確認を行う。 + +## 3. パラメータ設定 + +### input + +- `lot_size = 0.01`: 注文ロット。 +- `spread_limit = 60`: 許容スプレッド(point)。 +- `magic_number = 10001`: EA識別用マジックナンバー。 +- `initial_order = 0`: 起動直後の初回H4/H1処理を実行するか。`1` で有効。 +- `use_m15_entry_filter = true`: M15確定足によるエントリータイミング確認を使用するか。 +- `m15_entry_zone_atr_multiplier = 1.50`: M15平均レンジに対する候補価格接近許容倍率。 +- `use_m15_imbalance_confirmation = true`: T1/T3の順張り候補に対して、M15確定足の初動確認を追加するか。 +- `m15_imbalance_avg_body_period = 20`: M15初動確認で使う平均実体計算期間。 +- `m15_imbalance_sensitivity = 2.0`: M15初動確認で現在足実体が平均実体を上回る必要倍率。 +- `m15_imbalance_min_avg_body_points = 1.0`: M15平均実体の最小値(point)。 +- `use_m15_imbalance_debug_log = false`: M15初動確認の詳細ログを出すか。 +- `input_entry_max_candidate_age_minutes = 120`: H1候補価格を新規発注に使用できる最大経過分数。古い候補でM15条件が後から整った場合の遅延エントリーを抑止する。 +- `input_position_limit = 10`: 自EAの未約定注文 + ポジション数の実運用上限。内部上限 `POSITION_LIMIT` を超える指定は丸める。 +- `use_split_entry_zone = false`: H1予測ゾーンを使った分割エントリーを有効化するか。 +- `split_entry_count = 3`: 分割エントリー本数。1〜10に丸めて使用する。 +- `split_lot_mode = SPLIT_LOT_TOTAL`: 分割ロット配分モード。`SPLIT_LOT_TOTAL` は総ロットをN分割、`SPLIT_LOT_FIXED` は各注文に固定ロットを使う。 +- `split_total_lot_size = 0.09`: 総量分割モードの合計ロット。 +- `split_fixed_lot_size = 0.01`: 固定ロットモードの1注文ロット。 +- `cancel_old_split_pending_on_new_zone = true`: 新しいH1ゾーン読込時に、同じEAの古い分割pending注文を取消するか。 +- `input_sltp_manager_enabled = false`: UI連動の `CSLTPManager` による既存ポジションSL管理を有効化するか。 +- `input_sltp_show_panel = true`: チャート上にSLTP操作パネルを表示するか。 +- `input_sltp_use_breakeven = false`: 通常ブレークイーブンを有効化するか。 +- `input_sltp_breakeven_trigger_pips = 30.0`: 通常ブレークイーブンを開始する含み益pips。 +- `input_sltp_breakeven_buffer_pips = 3.0`: 通常ブレークイーブン時に建値から利益方向へずらすpips。 +- `input_sltp_use_elapsed_breakeven = false`: 保有時間ベースのブレークイーブンを有効化するか。 +- `input_sltp_elapsed_breakeven_hours = 4.0`: 保有時間ベースのブレークイーブンを許可するまでの保有時間。 +- `input_sltp_elapsed_breakeven_buffer_pips = 3.0`: 保有時間ベースのブレークイーブン時に建値から利益方向へずらすpips。 +- `input_sltp_use_active_trailing = false`: アクティブトレーリングを有効化するか。 +- `input_sltp_active_breakeven_pips = 30.0`: アクティブトレーリング開始pips。 +- `input_sltp_active_stop_loss_offset_pips = 5.0`: 開始時に建値から固定する利益pips。 +- `input_sltp_active_step_trigger_pips = 10.0`: SLを1段進めるために必要な追加利益pips。 +- `input_sltp_active_step_move_pips = 5.0`: 1段ごとにSLを利益方向へ動かすpips。 +- `input_sltp_use_tp_progress_stop = false`: TP進捗率に応じたSL固定を有効化するか。 +- `input_sltp_tp_progress_trigger_percent = 70.0`: TP進捗率SLを開始する進捗率。 +- `input_sltp_tp_progress_sl_lock_percent = 30.0`: 建値からTPまでの距離のうちSLで固定する割合。 +- `input_sltp_use_high_volatility_limit = false`: 短時間急変時のSL引き締めを有効化するか。 + +### 主要定数 + +- `POSITION_LIMIT = 48`: 自EAの未約定注文 + ポジション数の上限。 +- `ENTRY_H1_LIMIT = 2`: 未約定注文の有効期限(H1本数)。 +- `CLOSE_H1_LIMIT = 12`: ポジションの時間制限クローズ(H1本数)。 +- `HISTORY_BARS = 72`: Pythonへ渡すOHLC本数。 +- `OHLC_START_SHIFT = 1`: 確定足からOHLCを取得するためのシフト。 +- `M15_CONFIRM_BARS = 30`: M15平均レンジ計算に使う足数。 +- `ENTRY_RETRY_SECONDS = 60`: エントリー判定リトライ間隔。 +- `ENTRY_RETRY_LIMIT = 10`: H1候補価格に対する最大リトライ回数。 +- `TARGET_SIZE = 13`: `target_prices.txt` から読み込む数値数。 +- `TARGET_ZONE_SCHEMA_VERSION = 2`: `target_zones.txt` の形式バージョン。 +- `PYTHON_TIMEOUT_SECONDS = 600`: Python完了待ちの監視上限。 + +### Python側エントリー候補ガード + +- H1候補価格生成後、Python側で現在価格から遠すぎる `entry` を無効化する。 +- 距離上限は戦略別に計算し、T1/T3のStop系は `max(H1 EATR * 1.50, 5.00)`、T2/T4のLimit系は `max(H1 EATR * 1.00, 5.00)` とする。上限を超えた戦略は `0.00,0.00,0.00` に補正する。 +- このガードは、深い指値や遠いブレイク待ちがM15確認後に遅れて発注されることを抑えるための後処理である。 + +## 4. ファイル構成 + +- `Experts/MyProject/HIT-EA_refactor_ver6.mq5`: EAエントリーポイント。`OnInit`, `OnDeinit`, `OnTick`、input、グローバル状態、DLL importを保持する。 +- `Include/MyLib/Common/HITRuntimeController.mqh`: ティック処理、H4/H1/M15更新制御、ステータス表示、スプレッド判定。 +- `Include/MyLib/Signals/HITEntrySignal.mqh`: エントリー前提条件、注文タイプ別価格判定、M15フィルタ、リトライ状態更新。 +- `Include/MyLib/Common/HITExternalProcess.mqh`: done/runningファイル、外部プロセスハンドル、PID復元、終了コード確認、タイムアウト復旧。 +- `Include/MyLib/Signals/HITPythonSignalGateway.mqh`: OHLC CSV出力、Pythonバッチ起動、`trend_state.txt` / `target_prices.txt` / `target_zones.txt` 読込。 +- `Include/MyLib/Trading/HITTradeManager.mqh`: 注文送信、執行ポリシー、有効期限設定、注文/ポジション数集計、期限切れ取消/クローズ。 +- `Include/MyLib/Trading/SLTPManager.mqh`: 既存ポジションに対するブレークイーブン、時間経過BE、アクティブトレーリング、TP進捗SL、急変時SL引き締めを管理する。 +- `Include/MyLib/Panel/SLTPManagerPanel.mqh`: `CSLTPManager` の各設定をチャート上で切り替え、数値入力を検証してEAへ反映する操作パネル。 + +## 5. Python連携ファイル + +EAは `OnInit()` で `_Symbol` と `magic_number` から `HIT__` 形式の接頭辞を作り、MT5 `Files` 配下の連携ファイル名へ付与する。例: `HIT_GOLD_10001_ohlc_H4.csv`。batには同じ接頭辞を第1引数で渡し、bat側が `MT5_EA_FILE_PREFIX` としてPythonへ引き継ぐ。第2引数には `_Symbol` の `SYMBOL_DIGITS` を渡し、bat側が `MT5_PRICE_DIGITS` としてPythonへ引き継ぐ。 + +手動実行などで接頭辞が指定されない場合、Pythonは従来どおり `ohlc_H4.csv` などの旧ファイル名を使う。 + +### H4トレンド判定 + +- MQL5出力: `_ohlc_H4.csv` +- Python出力: `_trend_state.txt` +- 完了フラグ: `_process_done_trend.txt` +- 実行中フラグ: `_process_running_trend.txt` +- 起動バッチ: `\MQL5\python_for_ea\bat\get_trend_reply.bat` + +### H1エントリー価格生成 + +- MQL5出力: `_ohlc_H1.csv` +- Python出力: `_target_prices.txt`, `_target_zones.txt` +- 完了フラグ: `_process_done_entry.txt` +- 実行中フラグ: `_process_running_entry.txt` +- 起動バッチ: `\MQL5\python_for_ea\bat\get_entry_reply.bat` + +EAは `OnInit()` で `TerminalInfoString(TERMINAL_DATA_PATH)` から `MQL5\python_for_ea` を解決し、各batファイルの絶対パスを組み立てる。batファイルは自身の位置からPythonプロジェクトルートを解決するため、`C:\ea_py` には依存しない。 + +`target_prices.txt` は13行構成とする。 + +```text +1行目: res_chk +2-4行目: T1 Buy Stop の entry/tp/sl +5-7行目: T2 Buy Limit の entry/tp/sl +8-10行目: T3 Sell Stop の entry/tp/sl +11-13行目: T4 Sell Limit の entry/tp/sl +``` + +`target_zones.txt` は分割エントリー用の7行構成とする。 + +```text +1行目: schema_version(2) +2行目: res_chk +3行目: candidate_id(H1確定足時刻由来) +4行目: T1 Buy Stop の strategy, zone_low, zone_high, tp, sl +5行目: T2 Buy Limit の strategy, zone_low, zone_high, tp, sl +6行目: T3 Sell Stop の strategy, zone_low, zone_high, tp, sl +7行目: T4 Sell Limit の strategy, zone_low, zone_high, tp, sl +``` + +Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えてから `process_done_entry.txt` を作成する。`target_zones.txt` の価格小数桁は `MT5_PRICE_DIGITS` に合わせ、未指定時は安全側で5桁を使う。分割エントリーが無効な場合、EAは従来どおり `target_prices.txt` を使用する。 + +## 6. エントリー条件 + +新規エントリーは、以下をすべて満たす場合のみ行う。 + +- スプレッドが `spread_limit` 以下。 +- H4トレンド判定結果が完了済みで、`trend_state.txt` を読み込める。 +- H1候補価格生成が完了済みで、`target_prices.txt` を読み込める。 +- `res_chk = 1`。 +- H1候補価格が、元になったH1確定足時刻から `ENTRY_H1_LIMIT` 内で期限切れではない。 +- H1候補価格が、元になったH1確定足時刻から `input_entry_max_candidate_age_minutes` 分を超えていない。 +- 対象注文タイプの `entry/tp/sl` がすべて `0.0` より大きい。 +- H4 `market_state` に対して注文タイプが許可されている。 +- 注文タイプごとの価格整合条件とブローカー最小距離制約を満たす。 +- `use_m15_entry_filter = true` の場合、T2/T4はM15確定足の方向・反発・接近条件を満たす。T1/T3は `use_m15_imbalance_confirmation = true` の場合にM15初動確認を満たす。 +- 自EAの注文 + ポジション数が `input_position_limit` 未満。 +- 分割エントリー有効時は `target_zones.txt` の `res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + slot の重複注文/ポジション不存在を満たす。 + +### market_state と注文タイプ + +- `0 MARKET_LOW_VOL_RANGE`: T2 Buy Limit / T4 Sell Limit を許可。 +- `1 MARKET_HIGH_VOL_RANGE`: T2 Buy Limit / T4 Sell Limit を許可。 +- `2 MARKET_LOW_VOL_UP`: T1 Buy Stop / T2 Buy Limit を許可。 +- `3 MARKET_HIGH_VOL_UP`: T1 Buy Stop / T2 Buy Limit を許可。 +- `4 MARKET_LOW_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。 +- `5 MARKET_HIGH_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。 +- `6 MARKET_TECHNICAL_ERROR_STOP`: 新規注文を停止。 + +### 注文タイプごとの価格整合条件 + +- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry` +- T2 Buy Limit: `Ask > entry`, `tp > entry`, `sl < entry` +- T3 Sell Stop: `Bid > entry`, `tp < entry`, `sl > entry` +- T4 Sell Limit: `Bid < entry`, `tp < entry`, `sl > entry` + +## 7. エグジット条件 + +- 未約定注文は、注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。 +- 保有ポジションは、建玉時刻から `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合にクローズする。 +- 時間制限処理は `OnTick` の早い段階で実行し、Python完了待ちやスプレッド判定に依存させない。 +- EA削除時は `OnDeinit()` で外部プロセス状態とSLTPパネルを解放し、`Comment("")` と `ChartRedraw(0)` によりチャート上のステータス表示を消去する。 +- `input_sltp_manager_enabled` またはパネルの `Manager` がONの場合、時間制限処理後に `CSLTPManager::ManagePositions()` と `HighVolatilityLimit()` を実行する。これにより、Python結果待ちや新規注文停止中でも既存ポジションのSL保護を継続する。 +- 通常ブレークイーブンとアクティブトレーリングは同時有効化しない。パネル上では片方をONにするともう片方をOFFにし、`CSLTPManager::ValidateSettings()` でも排他条件を検証する。 + +## 7.1 SLTP操作パネル + +- パネルは `input_sltp_show_panel = true` の場合に作成され、チャート右上に表示される。 +- パネル生成に失敗した場合はログへ出力し、EA本体はinputで反映済みのSLTP設定のまま継続する。 +- `Manager` ボタンでSLTP管理全体のON/OFFを切り替える。OFFの場合、パネル値は保持するが `OnTick` でSL変更は行わない。 +- `BreakEven`, `Elapsed BE`, `Active Trail`, `TP Progress`, `High Vol` ボタンで各機能を切り替える。 +- 数値入力欄は `APPLY` 押下時に範囲検証される。範囲外または数値以外の場合は `MessageBox` とログで通知し、直前の有効値へ戻す。 +- `APPLY` 成功時のみEA内部の `CSLTPManager` 設定へ反映する。反映後は `ValidateSettings()` を通し、失敗した場合はSLTP管理を停止してログへ理由を出力する。 + +## 8. リスク管理 + +- 注文/ポジション管理対象は `_Symbol` と `magic_number` が一致するものに限定する。 +- `input_position_limit` は口座全体ではなく、自EA対象分のみを数える。内部の絶対上限は `POSITION_LIMIT` とする。 +- 送信時は `SYMBOL_FILLING_MODE` を参照し、IOC、FOK、RETURNの順で利用可能な執行ポリシーを選択する。 +- 通常注文と分割注文の送信前に、ロットが `SYMBOL_VOLUME_MIN/MAX/STEP` を満たすことを検証する。 +- ペンディング注文には、ブローカーが対応する場合にサーバー側の期限を設定する。 +- `OrderSend` の戻り値と `MqlTradeResult.retcode` を確認し、失敗時は `GetLastError()` またはretcodeをログへ出力する。 +- 分割エントリーの注文コメントには `candidate_id` とslot番号を入れ、同一slotの二重発注を抑止する。 +- `cancel_old_split_pending_on_new_zone = true` の場合、新しい有効 `candidate_id` を読んだ時は旧candidateの分割pending注文を取消し、`target_zones.txt` が無効または停止値の場合は既存の分割pending注文をすべて取消する。 +- SLTP管理は `_Symbol` と `magic_number` が一致する既存ポジションのみを対象とし、TPは既存値を維持する。 +- SL変更は既存SLより利益保護方向へ改善する場合だけ実行し、ブローカーのstop level / freeze levelを満たさない候補は送信しない。 +- `HighVolatilityLimit()` はBUYをBid、SELLをAsk基準で評価し、GOLD/XAUUSDはサフィックス付きシンボルでも1 pip = 0.1として扱う。 + +## 9. 異常系の扱い + +- `trend_state.txt` が存在しない、読み込めない、整数として厳格にパースできない、または `0..6` 以外の場合は `MARKET_TECHNICAL_ERROR_STOP` として扱う。 +- `target_prices.txt` が存在しない、読み込めない、13行未満、またはいずれかの行が数値として厳格にパースできない場合は `res_chk = 0` として扱う。 +- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。 +- Pythonプロセスが異常終了した場合、終了コードをログへ出力し、次回トリガーで再実行できる状態に戻す。 +- Pythonプロセスが `PYTHON_TIMEOUT_SECONDS` を超えても実行中の場合、`taskkill /T /F` でcmd/bat配下のプロセスツリーを終了し、done/runningファイルを整理して次回トリガーで再実行できる状態に戻す。 +- `running` ファイルだけが残っている場合は、PIDからプロセス復元を試みる。復元できずタイムアウト済みなら古いマーカーとして削除する。 + +## 10. 変更履歴 + +### 2026-05-30 + +- 同一Terminal内の複数EA/複数シンボルでPython連携ファイルが衝突しないよう、`HIT__` 接頭辞をMT5/Python双方のファイル名へ適用する仕様に更新した。 +- T1/T3の順張り候補でも `use_m15_imbalance_confirmation = true` の場合はM15初動確認を必須とする仕様に更新した。 +- `trend_state.txt`、`target_prices.txt`、`target_zones.txt` の数値読込を厳格化し、不正値は安全側の停止値として扱う仕様を追加した。 +- `target_zones.txt` が無効または停止値の場合に、古い分割pending注文を全取消できる仕様を追加した。 +- 現行コードに合わせて、`input_entry_max_candidate_age_minutes`、`ENTRY_H1_LIMIT`、SLTPバッファ、Python側距離ガードの既定値を修正した。 +- Pythonタイムアウト時に実行中プロセスツリーを強制終了して再試行できる仕様を追加した。 +- H1候補の鮮度判定をPython結果の読込時刻ではなく、`candidate_id` から復元したH1確定足時刻基準へ変更した。 +- `MT5_PRICE_DIGITS` によりPython側の `target_zones.txt` 出力桁数をシンボル桁数へ合わせる仕様を追加した。 +- 通常注文ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 検証、OHLC CSVのNaN/inf・OHLC整合性検証、SELL高ボラSLのAsk基準評価、GOLD/XAUUSDサフィックス対応を追加した。 + +### 2026-05-27 + +- Python補助アプリの配置を `C:\ea_py` からMT5データフォルダ配下の `MQL5\python_for_ea` へ移行する仕様に変更した。 +- EA側は `TerminalInfoString(TERMINAL_DATA_PATH)` からbatパスを組み立て、外部プロセスの作業ディレクトリはbatファイルの配置から導出するようにした。 +- Python側は `MQL5\python_for_ea` の親にある `MQL5\Files` を既定の連携先として自動検出し、必要に応じて `MT5_FILES_DIR` または `MT5_DATA_PATH` で上書きできる仕様にした。 + +### 2026-05-24 + +- Python側で `target_zones.txt` を追加出力し、既存 `target_prices.txt` と両方を書き終えてから `process_done_entry.txt` を作成する仕様にした。 +- EA側に `use_split_entry_zone`、`split_entry_count`、`split_lot_mode`、`split_total_lot_size`、`split_fixed_lot_size`、`input_position_limit` を追加した。 +- 分割エントリー有効時は、H1予測ゾーンをN本に分割し、総ロット分割または固定ロットでpending注文を出せるようにした。 +- `candidate_id` + slot番号で分割注文を識別し、同一slotの二重発注を抑止する仕様を追加した。 + +### 2026-05-13 + +- Python側にH1 EATRベースの候補距離ガードを追加し、現在価格から遠すぎる候補をMT5へ渡す前に無効化する仕様を追加。 +- EA側に `input_entry_max_candidate_age_minutes` を追加し、M15確認が遅れて整った古いH1候補で新規発注しないようにした。 +- エントリー見送りログへH1候補の経過秒数を追加し、遅延・M15未確認・価格不整合の原因を追跡しやすくした。 + +### 2026-05-05 + +- EA削除後にチャート左上のステータスコメントが残らないよう、`OnDeinit()` で `Comment("")` と `ChartRedraw(0)` を実行する終了処理を追加。 +- `HIT-EA_refactor_ver5.mq5` に `CSLTPManager` と専用チャートパネル `SLTPManagerPanel.mqh` を接続し、UIからブレークイーブン、時間経過BE、アクティブトレーリング、TP進捗SL、急変時SL引き締めを操作できるようにした。 +- SLTP管理を `OnTick` の時間制限処理直後に実行し、Python結果待ちやスプレッド判定に依存せず既存ポジション保護を継続する仕様を追加。 +- SLTPパネル用inputと、通常ブレークイーブン/アクティブトレーリングの排他検証、`APPLY` 時の数値検証を追加。 +- `HIT-EA_refactor_ver5.mq5` の関数群を用途別 `.mqh` に分割。 +- `OnInit`, `OnDeinit`, `OnTick`、input、グローバル状態はEA本体に残し、動作ロジックは `Include/MyLib/` 配下へ移動。 +- 現行コードに合わせて、`CreateProcessW` ベースのPythonプロセス監視、M15フィルタ、`market_state` 0..6、`spread_limit = 60` を仕様へ反映。 + +### 2026-05-02 + +- `res_chk=0` 時にエントリー送信を抑止する分岐を追加。 +- 時間制限処理を、Python完了待ち・スプレッド制限より前に移動。 +- `initial_order` の初回処理フラグをH4用・H1用に分離。 +- OHLC取得を `CopyRates` 中心に変更し、確定足のみをPythonへ渡す仕様に変更。