diff --git a/Experts/MyProject/HIT-EA_refactor_ver6.mq5 b/Experts/MyProject/HIT-EA_refactor_ver6.mq5 index 653da17..fb351de 100644 --- a/Experts/MyProject/HIT-EA_refactor_ver6.mq5 +++ b/Experts/MyProject/HIT-EA_refactor_ver6.mq5 @@ -61,8 +61,11 @@ long OpenProcess(uint dwDesiredAccess, int bInheritHandle, uint dwProcessId); #import // バッチファイルのパス -string get_trend_reply_bat = "C:\\ea_py\\bat\\get_trend_reply.bat"; // H4: trend -string get_entry_reply_bat = "C:\\ea_py\\bat\\get_entry_reply.bat"; // H1: entry +// 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 done_trend_file = "process_done_trend.txt"; @@ -228,6 +231,54 @@ 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 Python連携で使用するbatパスを初期化します。 + */ +void ConfigurePythonGatewayPaths() + { + 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); + } + //+------------------------------------------------------------------+ //| 起動時の処理 //+------------------------------------------------------------------+ @@ -241,6 +292,8 @@ bool g_sltp_panel_created = false; int OnInit() // プロセス完了ファイルと実行中ファイルの状態を初期化 { + ConfigurePythonGatewayPaths(); + PrepareDoneFileOnInit(done_trend_file, running_trend_file, "trend"); PrepareDoneFileOnInit(done_entry_file, running_entry_file, "entry"); diff --git a/Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260527_231529 b/Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260527_231529 new file mode 100644 index 0000000..653da17 --- /dev/null +++ b/Experts/MyProject/HIT-EA_refactor_ver6.mq5.bak_codex_20260527_231529 @@ -0,0 +1,462 @@ +//+------------------------------------------------------------------+ +//| 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 + +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 + +// バッチファイルのパス +string get_trend_reply_bat = "C:\\ea_py\\bat\\get_trend_reply.bat"; // H4: trend +string get_entry_reply_bat = "C:\\ea_py\\bat\\get_entry_reply.bat"; // H1: entry + +// プロセス完了ファイルの設定 +string done_trend_file = "process_done_trend.txt"; +string done_entry_file = "process_done_entry.txt"; + +// Python出力ファイル +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 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; + +//+------------------------------------------------------------------+ + +//+------------------------------------------------------------------+ +//| 起動時の処理 +//+------------------------------------------------------------------+ +/** + * @brief EA起動時の初期化処理を行います。 + * + * Python連携用のdone/runningファイル状態を確認し、`initial_order` が有効な場合は + * H4トレンド判定とH1エントリー価格生成をそれぞれ初回実行対象にします。 + * H4/H1の初回フラグを分けることで、起動直後にH4処理だけが繰り返される状態を防ぎます。 + */ +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); + + 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/HITExternalProcess.mqh b/Include/MyLib/Common/HITExternalProcess.mqh index 0f3d84c..4ce2b0f 100644 --- a/Include/MyLib/Common/HITExternalProcess.mqh +++ b/Include/MyLib/Common/HITExternalProcess.mqh @@ -259,6 +259,55 @@ bool IsExternalProcessTimedOut(ExternalProcessState &process, const string runni //+------------------------------------------------------------------+ //| バッチファイルをプロセスハンドル付きで起動する関数 //+------------------------------------------------------------------+ +/** + * @brief パス内の最後のディレクトリ区切り文字位置を返します。 + */ +int LastPathSeparatorIndex(const string path) + { + for(int i = StringLen(path) - 1; i >= 0; i--) + { + int ch = StringGetCharacter(path, i); + if(ch == 92 || ch == 47) // backslash or slash + return i; + } + + return -1; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief ファイルパスから親ディレクトリを返します。 + */ +string ParentDirectory(const string path) + { + int index = LastPathSeparatorIndex(path); + if(index <= 0) + return ""; + + return StringSubstr(path, 0, index); + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ +/** + * @brief batファイルの位置からPythonアプリの作業ディレクトリを返します。 + */ +string BatchWorkingDirectory(const string bat_file) + { + string bat_dir = ParentDirectory(bat_file); + int length = StringLen(bat_dir); + if(length >= 4 && StringSubstr(bat_dir, length - 4) == "\\bat") + return StringSubstr(bat_dir, 0, length - 4); + + return bat_dir; + } + +//+------------------------------------------------------------------+ +//| | +//+------------------------------------------------------------------+ bool StartBatchProcess(const string bat_file, const string running_file, const string label, ExternalProcessState &process) { STARTUPINFO_W startup_info = {}; @@ -268,6 +317,7 @@ bool StartBatchProcess(const string bat_file, const string running_file, const s string cmd_exe = "C:\\Windows\\System32\\cmd.exe"; string command_line = "\"" + cmd_exe + "\" /c \"\"" + bat_file + "\"\""; + string current_directory = BatchWorkingDirectory(bat_file); int created = CreateProcessW( cmd_exe, @@ -277,14 +327,15 @@ bool StartBatchProcess(const string bat_file, const string running_file, const s 0, CREATE_NO_WINDOW, 0, - "C:\\ea_py", + current_directory, startup_info, process_info ); if(created == 0 || process_info.hProcess == 0) { - Print("[", label, "] CreateProcessW failed. file=", bat_file, " err=", GetLastError()); + Print("[", label, "] CreateProcessW failed. file=", bat_file, + " cwd=", current_directory, " err=", GetLastError()); ResetExternalProcessState(process); DeleteRunningFile(running_file); return false; @@ -301,7 +352,8 @@ bool StartBatchProcess(const string bat_file, const string running_file, const s process.started_at = TimeCurrent(); CreateRunningFile(running_file, process.process_id); - Print("[", label, "] process started. pid=", process.process_id, " file=", bat_file); + Print("[", label, "] process started. pid=", process.process_id, + " file=", bat_file, " cwd=", current_directory); return true; } diff --git a/Include/MyLib/Common/HITExternalProcess.mqh.bak_codex_20260527_231529 b/Include/MyLib/Common/HITExternalProcess.mqh.bak_codex_20260527_231529 new file mode 100644 index 0000000..0f3d84c --- /dev/null +++ b/Include/MyLib/Common/HITExternalProcess.mqh.bak_codex_20260527_231529 @@ -0,0 +1,528 @@ +#ifndef HIT_EXTERNAL_PROCESS_MQH +#define HIT_EXTERNAL_PROCESS_MQH + +//+------------------------------------------------------------------+ +//| "process_done.txt"を作成する関数 +//+------------------------------------------------------------------+ +/** + * @brief 指定したdoneファイルを作成します。 + * + * @param name 作成するファイル名。MT5のMQL5\Files配下を基準に扱います。 + */ +void CreateDoneFile(const string name) + { + if(!FileIsExist(name)) + { + int h = FileOpen(name, FILE_WRITE|FILE_TXT); + if(h != INVALID_HANDLE) + FileClose(h); + else + Print("Failed to create file: ", name, " err=", GetLastError()); + } + } + +//+------------------------------------------------------------------+ +//| "process_done.txt"を削除する関数 +//+------------------------------------------------------------------+ +/** + * @brief 指定したdoneファイルを削除します。 + * + * @param name 削除するファイル名。存在しない場合は何もしません。 + */ +void DeleteDoneFile(const string name) + { + if(FileIsExist(name)) + FileDelete(name); + } + + +//+------------------------------------------------------------------+ +//| "process_done.txt"の存在を確認し、存在する場合はtrueを返す関数 +//+------------------------------------------------------------------+ +/** + * @brief 指定したdoneファイルの存在有無を確認します。 + * + * @param name 確認するファイル名。 + * @return ファイルが存在する場合はtrue、存在しない場合はfalse。 + */ +bool CheckDoneFile(const string name) + { + return FileIsExist(name); + } + +//+------------------------------------------------------------------+ +//| Python実行中ファイルを作成する関数 +//+------------------------------------------------------------------+ +/** + * @brief Python起動時刻を実行中ファイルへ記録します。 + * + * @param name 作成するrunningファイル名。 + */ +void CreateRunningFile(const string name, const uint process_id) + { + int h = FileOpen(name, FILE_WRITE | FILE_TXT); + if(h != INVALID_HANDLE) + { + FileWrite(h, IntegerToString((long)TimeCurrent())); + FileWrite(h, IntegerToString((long)process_id)); + FileClose(h); + } + else + Print("Failed to create running file: ", name, " err=", GetLastError()); + } + +//+------------------------------------------------------------------+ +//| Python実行中ファイルを削除する関数 +//+------------------------------------------------------------------+ +/** + * @brief 指定したrunningファイルを削除します。 + * + * @param name 削除するrunningファイル名。 + */ +void DeleteRunningFile(const string name) + { + if(FileIsExist(name)) + FileDelete(name); + } + +//+------------------------------------------------------------------+ +//| Python実行開始時刻を読み込む関数 +//+------------------------------------------------------------------+ +/** + * @brief runningファイルに記録されたPython起動時刻を読み込みます。 + * + * @param name 読み込むrunningファイル名。 + * @return 読み込めた起動時刻。失敗時は0。 + */ +datetime LoadRunningStartedAt(const string name) + { + if(!FileIsExist(name)) + return 0; + + int h = FileOpen(name, FILE_READ | FILE_TXT); + if(h == INVALID_HANDLE) + { + Print("Failed to open running file: ", name, " err=", GetLastError()); + return 0; + } + + string line = FileReadString(h); + FileClose(h); + return (datetime)StringToInteger(line); + } + +//+------------------------------------------------------------------+ +//| Python実行プロセスIDを読み込む関数 +//+------------------------------------------------------------------+ +/** + * @brief runningファイルに記録されたプロセスIDを読み込みます。 + * + * @param name 読み込むrunningファイル名。 + * @return 読み込めたプロセスID。旧形式または失敗時は0。 + */ +uint LoadRunningProcessId(const string name) + { + if(!FileIsExist(name)) + return 0; + + int h = FileOpen(name, FILE_READ | FILE_TXT); + if(h == INVALID_HANDLE) + { + Print("Failed to open running file: ", name, " err=", GetLastError()); + return 0; + } + + FileReadString(h); // started_at + if(FileIsEnding(h)) + { + FileClose(h); + return 0; + } + + string line = FileReadString(h); + FileClose(h); + return (uint)StringToInteger(line); + } + +//+------------------------------------------------------------------+ +//| Python実行中ファイルがタイムアウトしているか判定する関数 +//+------------------------------------------------------------------+ +/** + * @brief runningファイルの起動時刻からPython待ち上限を超えているか判定します。 + * + * @param name 判定するrunningファイル名。 + * @return タイムアウトしている場合はtrue。 + */ +bool IsRunningFileTimedOut(const string name) + { + datetime started_at = LoadRunningStartedAt(name); + if(started_at <= 0) + return true; + + return (TimeCurrent() - started_at >= PYTHON_TIMEOUT_SECONDS); + } + +//+------------------------------------------------------------------+ +//| 外部プロセス状態を初期値に戻す関数 +//+------------------------------------------------------------------+ +void ResetExternalProcessState(ExternalProcessState &process) + { + if(process.handle != 0) + CloseHandle(process.handle); + + process.handle = 0; + process.process_id = 0; + process.active = false; + process.exit_code_ready = false; + process.exit_code = 0; + process.started_at = 0; + } + +//+------------------------------------------------------------------+ +//| runningファイルのPIDからプロセスハンドルを復元する関数 +//+------------------------------------------------------------------+ +bool AttachRunningProcess(const string running_file, const string label, ExternalProcessState &process) + { + if(process.active && process.handle != 0) + return true; + + uint process_id = LoadRunningProcessId(running_file); + if(process_id == 0) + return false; + + long handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, process_id); + if(handle == 0) + { + Print("[", label, "] failed to open running process. pid=", process_id, " err=", GetLastError()); + return false; + } + + process.handle = handle; + process.process_id = process_id; + process.active = true; + process.exit_code_ready = false; + process.exit_code = STILL_ACTIVE; + process.started_at = LoadRunningStartedAt(running_file); + return true; + } + +//+------------------------------------------------------------------+ +//| 外部プロセスの終了状態を更新する関数 +//+------------------------------------------------------------------+ +bool UpdateExternalProcessStatus(ExternalProcessState &process, const string label) + { + if(!process.active || process.handle == 0) + return true; + + int wait_result = WaitForSingleObject(process.handle, 0); + if(wait_result == WAIT_TIMEOUT) + return false; + + uint exit_code = 1; + if(wait_result == WAIT_OBJECT_0) + { + if(GetExitCodeProcess(process.handle, exit_code) == 0) + { + Print("[", label, "] failed to get process exit code. err=", GetLastError()); + exit_code = 1; + } + } + else + { + Print("[", label, "] WaitForSingleObject failed. result=", wait_result, " err=", GetLastError()); + exit_code = 1; + } + + CloseHandle(process.handle); + process.handle = 0; + process.active = false; + process.exit_code_ready = true; + process.exit_code = exit_code; + Print("[", label, "] process finished. pid=", process.process_id, " exit_code=", exit_code); + return true; + } + +//+------------------------------------------------------------------+ +//| 外部プロセスがタイムアウトしているか判定する関数 +//+------------------------------------------------------------------+ +bool IsExternalProcessTimedOut(ExternalProcessState &process, const string running_file) + { + datetime started_at = process.started_at; + if(started_at <= 0) + started_at = LoadRunningStartedAt(running_file); + if(started_at <= 0) + return true; + + return (TimeCurrent() - started_at >= PYTHON_TIMEOUT_SECONDS); + } + +//+------------------------------------------------------------------+ +//| バッチファイルをプロセスハンドル付きで起動する関数 +//+------------------------------------------------------------------+ +bool StartBatchProcess(const string bat_file, const string running_file, const string label, ExternalProcessState &process) + { + STARTUPINFO_W startup_info = {}; + PROCESS_INFORMATION process_info = {}; + + startup_info.cb = (uint)sizeof(startup_info); + + string cmd_exe = "C:\\Windows\\System32\\cmd.exe"; + string command_line = "\"" + cmd_exe + "\" /c \"\"" + bat_file + "\"\""; + + int created = CreateProcessW( + cmd_exe, + command_line, + 0, + 0, + 0, + CREATE_NO_WINDOW, + 0, + "C:\\ea_py", + startup_info, + process_info + ); + + if(created == 0 || process_info.hProcess == 0) + { + Print("[", label, "] CreateProcessW failed. file=", bat_file, " err=", GetLastError()); + ResetExternalProcessState(process); + DeleteRunningFile(running_file); + return false; + } + + if(process_info.hThread != 0) + CloseHandle(process_info.hThread); + + process.handle = process_info.hProcess; + process.process_id = process_info.dwProcessId; + process.active = true; + process.exit_code_ready = false; + process.exit_code = STILL_ACTIVE; + process.started_at = TimeCurrent(); + + CreateRunningFile(running_file, process.process_id); + Print("[", label, "] process started. pid=", process.process_id, " file=", bat_file); + return true; + } + +//+------------------------------------------------------------------+ +//| EA起動時のdoneファイル状態を整える関数 +//+------------------------------------------------------------------+ +/** + * @brief EA起動時にPython実行中状態を壊さない範囲でdoneファイルを準備します。 + * + * @param done_file 完了判定ファイル名。 + * @param running_file 実行中判定ファイル名。 + * @param label ログ表示用ラベル。 + */ +void PrepareDoneFileOnInit(const string done_file, const string running_file, const string label) + { + if(CheckDoneFile(done_file)) + { + DeleteRunningFile(running_file); + return; + } + + if(FileIsExist(running_file)) + { + if(IsRunningFileTimedOut(running_file)) + { + Print("[", label, "] stale running file found on init. Remove marker without creating done."); + DeleteRunningFile(running_file); + } + else + Print("[", label, "] Python seems to be running on init. Keep waiting."); + return; + } + + Print("[", label, "] done file not found on init. Keep as not-ready until the next trigger."); + } + +//+------------------------------------------------------------------+ +//| Python完了状態を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief doneファイルがあれば完了扱いにし、runningファイルを片付けます。 + * + * @param done_file 完了判定ファイル名。 + * @param running_file 実行中判定ファイル名。 + * @return 完了済みの場合はtrue。 + */ +bool IsProcessResultReady(const string done_file, const string running_file, const string result_file, const string label, ExternalProcessState &process) + { + if(process.active || FileIsExist(running_file)) + { + if(process.active || AttachRunningProcess(running_file, label, process)) + { + if(!UpdateExternalProcessStatus(process, label)) + return false; + + DeleteRunningFile(running_file); + if(process.exit_code_ready && process.exit_code != 0) + { + Print("[", label, "] process failed. exit_code=", process.exit_code); + return false; + } + } + else if(!CheckDoneFile(done_file)) + { + return false; + } + } + + if(!CheckDoneFile(done_file)) + return false; + + if(!FileIsExist(result_file)) + { + Print("[", label, "] done exists but result file is missing: ", result_file); + return false; + } + + DeleteRunningFile(running_file); + return true; + } + +//+------------------------------------------------------------------+ +//| Python開始可能状態を返す関数 +//+------------------------------------------------------------------+ +/** + * @brief done/runningファイル状態からPythonを新規起動できるか判定します。 + * + * @param done_file 完了判定ファイル名。 + * @param running_file 実行中判定ファイル名。 + * @param label ログ表示用ラベル。 + * @return 起動可能な場合はtrue。 + */ +bool IsProcessStartAllowed(const string done_file, const string running_file, const string label, ExternalProcessState &process) + { + if(process.active || FileIsExist(running_file)) + { + if(process.active || AttachRunningProcess(running_file, label, process)) + { + if(!UpdateExternalProcessStatus(process, label)) + { + if(IsExternalProcessTimedOut(process, running_file)) + Print("[", label, "] Python timed out, but the process is still running. Keep waiting."); + else + Print("[", label, "] Python is still running. Skip execute."); + return false; + } + + DeleteRunningFile(running_file); + if(process.exit_code_ready && process.exit_code != 0) + Print("[", label, "] previous process failed. exit_code=", process.exit_code, ". Retry is allowed."); + } + else + { + if(CheckDoneFile(done_file)) + { + DeleteRunningFile(running_file); + return true; + } + + if(IsRunningFileTimedOut(running_file)) + { + Print("[", label, "] stale running marker without live process. Retry is allowed."); + DeleteRunningFile(running_file); + return true; + } + + Print("[", label, "] running marker exists, but process cannot be verified yet. Skip execute."); + return false; + } + } + + if(CheckDoneFile(done_file)) + { + DeleteRunningFile(running_file); + return true; + } + + Print("[", label, "] done file missing without running marker. Start new process."); + return true; + } + +//+------------------------------------------------------------------+ +//| タイムアウトしたPython待ちを復旧する関数 +//+------------------------------------------------------------------+ +/** + * @brief Pythonがdoneファイルを返さない状態を検知し、再実行できる状態へ戻します。 + */ +void RecoverTimedOutPythonProcesses() + { + if(RecoverTimedOutProcess(done_trend_file, running_trend_file, "trend", g_trend_process)) + { + g_ea.load_trend_flg = false; + g_init_trend_pending = true; + } + + if(RecoverTimedOutProcess(done_entry_file, running_entry_file, "entry", g_entry_process)) + { + g_ea.load_target_flg = false; + g_bars_H1_check = false; + g_bars_M15_check = false; + g_ea.chk_cnt = 0; + g_init_entry_pending = true; + } + } + +//+------------------------------------------------------------------+ +//| 個別Python処理のタイムアウトを復旧する関数 +//+------------------------------------------------------------------+ +/** + * @brief 1種類のPython処理について、実行中ファイルのタイムアウトを検知します。 + * + * @param done_file 完了判定ファイル名。 + * @param running_file 実行中判定ファイル名。 + * @param label ログ表示用ラベル。 + * @return タイムアウト復旧を行った場合はtrue。 + */ +bool RecoverTimedOutProcess(const string done_file, const string running_file, const string label, ExternalProcessState &process) + { + if(CheckDoneFile(done_file) && !process.active) + { + DeleteRunningFile(running_file); + return false; + } + + if(!process.active && !FileIsExist(running_file)) + return false; + + if(process.active || AttachRunningProcess(running_file, label, process)) + { + if(!UpdateExternalProcessStatus(process, label)) + { + if(IsExternalProcessTimedOut(process, running_file)) + Print("[", label, "] Python exceeded ", PYTHON_TIMEOUT_SECONDS, + " seconds, but the process is still running. Keep waiting."); + return false; + } + + DeleteRunningFile(running_file); + if(process.exit_code_ready && process.exit_code != 0) + { + Print("[", label, "] Python process failed. Retry on next trigger."); + return true; + } + + if(!CheckDoneFile(done_file)) + { + Print("[", label, "] Python process ended without done file. Retry on next trigger."); + return true; + } + + return false; + } + + if(!IsRunningFileTimedOut(running_file)) + return false; + + Print("[", label, "] Python did not finish within ", + PYTHON_TIMEOUT_SECONDS, " seconds, and no live process was found. Retry is allowed."); + DeleteRunningFile(running_file); + return true; + } + + +#endif diff --git a/docs/MyProject/HIT-EA_spec_en.md b/docs/MyProject/HIT-EA_spec_en.md index 8c65bd3..2d0f528 100644 --- a/docs/MyProject/HIT-EA_spec_en.md +++ b/docs/MyProject/HIT-EA_spec_en.md @@ -85,7 +85,7 @@ - Python output: `trend_state.txt` - Done marker: `process_done_trend.txt` - Running marker: `process_running_trend.txt` -- Batch file: `C:\ea_py\bat\get_trend_reply.bat` +- Batch file: `\MQL5\python_for_ea\bat\get_trend_reply.bat` ### H1 Entry Candidate Generation @@ -93,7 +93,9 @@ - Python output: `target_prices.txt` - Done marker: `process_done_entry.txt` - Running marker: `process_running_entry.txt` -- Batch file: `C:\ea_py\bat\get_entry_reply.bat` +- 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: @@ -175,6 +177,12 @@ New entries are allowed only when all conditions below are satisfied: ## 10. Changelog +### 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-13 - Added a Python-side H1 EATR distance guard that invalidates candidates farther than `max(H1 EATR * 0.65, 5.00)` from the current price before they are passed to MT5. diff --git a/docs/MyProject/HIT-EA_spec_ja.md b/docs/MyProject/HIT-EA_spec_ja.md index dedd318..4ae5f70 100644 --- a/docs/MyProject/HIT-EA_spec_ja.md +++ b/docs/MyProject/HIT-EA_spec_ja.md @@ -93,7 +93,7 @@ - Python出力: `trend_state.txt` - 完了フラグ: `process_done_trend.txt` - 実行中フラグ: `process_running_trend.txt` -- 起動バッチ: `C:\ea_py\bat\get_trend_reply.bat` +- 起動バッチ: `\MQL5\python_for_ea\bat\get_trend_reply.bat` ### H1エントリー価格生成 @@ -101,7 +101,9 @@ - Python出力: `target_prices.txt`, `target_zones.txt` - 完了フラグ: `process_done_entry.txt` - 実行中フラグ: `process_running_entry.txt` -- 起動バッチ: `C:\ea_py\bat\get_entry_reply.bat` +- 起動バッチ: `\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行構成とする。 @@ -200,6 +202,12 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて ## 10. 変更履歴 +### 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` を作成する仕様にした。 diff --git a/python_for_ea/AGENTS.md b/python_for_ea/AGENTS.md index 19d07ac..b07f860 100644 --- a/python_for_ea/AGENTS.md +++ b/python_for_ea/AGENTS.md @@ -2,7 +2,7 @@ ## 位置づけ -- このファイルは、`C:\ea_py` プロジェクトで作業するAIエージェント共通の正本とする。 +- このファイルは、MT5データフォルダ配下の `MQL5\python_for_ea` プロジェクトで作業するAIエージェント共通の正本とする。 - GitHub Copilot 固有の動作指示は、必要になった時点で `.github/copilot-instructions.md` に分離する。 - ファイル種別・技術領域ごとの詳細指示は、必要に応じて `.github/instructions/`、`.cursor/rules/`、`.agents/skills/` に分離する。 - 長文ルールを複数ファイルへ重複記載しない。共通ルールはこのファイルを優先する。 @@ -101,7 +101,7 @@ - 変更後は、影響範囲に応じて以下を確認する。 ```bash -uv run python -m py_compile C:/ea_py/get_trend_reply.py C:/ea_py/get_entry_reply.py +uv run python -m py_compile get_trend_reply.py get_entry_reply.py uv run ruff check . uv run ty check uv run pytest diff --git a/python_for_ea/README.md b/python_for_ea/README.md index 0aad915..e8ecea9 100644 --- a/python_for_ea/README.md +++ b/python_for_ea/README.md @@ -9,7 +9,7 @@ Python 側は注文送信を行いません。発注、注文管理、M15 確定 MT5 EA からは `bat/` 経由でルート直下の互換スクリプトを起動します。 ```text -C:/ea_py/ +/MQL5/python_for_ea/ ├─ get_trend_reply.py ├─ get_entry_reply.py ├─ bat/ @@ -21,20 +21,20 @@ C:/ea_py/ - `get_trend_reply.py`: `src/ea_py/pipelines/trend_pipeline.py` を起動する薄い入口。 - `get_entry_reply.py`: `src/ea_py/pipelines/entry_pipeline.py` を起動する薄い入口。 -- `bat/get_trend_reply.bat`: `uv run python C:\ea_py\get_trend_reply.py` を実行。 -- `bat/get_entry_reply.bat`: `uv run python C:\ea_py\get_entry_reply.py` を実行。 +- `bat/get_trend_reply.bat`: bat自身の親フォルダをプロジェクトルートとして `uv run python get_trend_reply.py` を実行。 +- `bat/get_entry_reply.bat`: bat自身の親フォルダをプロジェクトルートとして `uv run python get_entry_reply.py` を実行。 入口ファイル名や配置を変える場合は、`bat/` と MQL5 EA 側の呼び出し設定も合わせて更新してください。 ## MT5 連携ファイル -既定の連携先は次の MT5 `MQL5/Files` ディレクトリです。 +既定の連携先は、このプロジェクトの親にある MT5 `MQL5/Files` ディレクトリです。 ```text -C:/Users/new/AppData/Roaming/MetaQuotes/Terminal/5BDB0B60344C088C2FA5CA35699BAAFD/MQL5/Files/ +/MQL5/Files/ ``` -パスの組み立ては `src/ea_py/paths.py` で定義しています。 +パスの組み立ては `src/ea_py/paths.py` で定義しています。特殊な配置では `MT5_FILES_DIR` または `MT5_DATA_PATH` 環境変数で上書きできます。 ### 入力 @@ -174,14 +174,14 @@ uv sync 個別実行: ```powershell -uv run python C:/ea_py/get_trend_reply.py -uv run python C:/ea_py/get_entry_reply.py +uv run python get_trend_reply.py +uv run python get_entry_reply.py ``` 品質チェック: ```powershell -uv run python -m py_compile C:/ea_py/get_trend_reply.py C:/ea_py/get_entry_reply.py +uv run python -m py_compile get_trend_reply.py get_entry_reply.py uv run ruff check . uv run ty check uv run pytest diff --git a/python_for_ea/bat/get_entry_reply.bat b/python_for_ea/bat/get_entry_reply.bat index 486b3e3..71fdbeb 100644 --- a/python_for_ea/bat/get_entry_reply.bat +++ b/python_for_ea/bat/get_entry_reply.bat @@ -1,8 +1,11 @@ @echo off setlocal -set APP_DIR=C:\ea_py +chcp 65001 >nul +set PYTHONIOENCODING=utf-8 +set "APP_DIR=%~dp0.." set PY_FILE=get_entry_reply.py +for %%I in ("%APP_DIR%") do set "APP_DIR=%%~fI" cd /d "%APP_DIR%" diff --git a/python_for_ea/bat/get_trend_reply.bat b/python_for_ea/bat/get_trend_reply.bat index 257fb1c..8e06ff9 100644 --- a/python_for_ea/bat/get_trend_reply.bat +++ b/python_for_ea/bat/get_trend_reply.bat @@ -1,8 +1,11 @@ @echo off setlocal -set APP_DIR=C:\ea_py +chcp 65001 >nul +set PYTHONIOENCODING=utf-8 +set "APP_DIR=%~dp0.." set PY_FILE=get_trend_reply.py +for %%I in ("%APP_DIR%") do set "APP_DIR=%%~fI" cd /d "%APP_DIR%" diff --git a/python_for_ea/docs/architecture/folder-structure.md b/python_for_ea/docs/architecture/folder-structure.md index 40cab4f..7bf0290 100644 --- a/python_for_ea/docs/architecture/folder-structure.md +++ b/python_for_ea/docs/architecture/folder-structure.md @@ -2,7 +2,7 @@ ## 目的 -このドキュメントは、`C:\ea_py` のフォルダ構成に関する正本である。 +このドキュメントは、MT5データフォルダ配下の `MQL5\python_for_ea` のフォルダ構成に関する正本である。 現在のプロジェクトは、MT5 HIT-EAから出力されたOHLC CSVをPythonで解析し、OpenAI APIを使ってGOLD/XAUUSD向けの相場環境とエントリー候補価格を返す補助アプリケーションである。 @@ -11,7 +11,7 @@ MT5 EAから `bat/` 経由でルート直下のPythonスクリプトが呼ばれ ## 現在のフォルダ構成 ```text -C:/ea_py/ +/MQL5/python_for_ea/ ├─ AGENTS.md ├─ README.md ├─ pyproject.toml @@ -105,7 +105,7 @@ C:/Users/new/AppData/Roaming/MetaQuotes/Terminal/{terminal_ID}/MQL5/Files/ └─ tmp_chart_long.png ``` -これらは実行時生成物であり、原則として `C:\ea_py` 配下へコピーして正本化しない。 +これらは実行時生成物であり、原則として `MQL5\python_for_ea` 配下へコピーして正本化しない。 ## MQL5 EAファイル @@ -127,7 +127,7 @@ EAファイルはこのPythonプロジェクトの外部連携先として扱う ルート直下の `get_trend_reply.py` と `get_entry_reply.py` は、MT5/bat互換のため薄いエントリーポイントとして残す。 ```text -C:/ea_py/ +/MQL5/python_for_ea/ ├─ AGENTS.md ├─ README.md ├─ pyproject.toml diff --git a/python_for_ea/src/ea_py/paths.py b/python_for_ea/src/ea_py/paths.py index 440c529..89c96e9 100644 --- a/python_for_ea/src/ea_py/paths.py +++ b/python_for_ea/src/ea_py/paths.py @@ -3,19 +3,24 @@ from __future__ import annotations from dataclasses import dataclass +import os from pathlib import Path DEFAULT_USER_NAME = "new" DEFAULT_TERMINAL_ID = "5BDB0B60344C088C2FA5CA35699BAAFD" +ENV_MT5_FILES_DIR = "MT5_FILES_DIR" +ENV_MT5_DATA_PATH = "MT5_DATA_PATH" @dataclass(frozen=True) class Mt5PathSettings: """MT5データフォルダを特定するための設定。""" - user_name: str = DEFAULT_USER_NAME - terminal_id: str = DEFAULT_TERMINAL_ID + files_dir: Path | None = None + data_path: Path | None = None + user_name: str | None = None + terminal_id: str | None = None @dataclass(frozen=True) @@ -43,17 +48,76 @@ class EntryPaths: debug_reason: Path +def discover_mql5_dir(start_path: Path | None = None) -> Path | None: + """このPythonプロジェクトの配置からMQL5ルートを探す。""" + path = (start_path or Path(__file__)).resolve() + candidates = [path, *path.parents] + for candidate in candidates: + if candidate.name.lower() == "mql5": + return candidate + + return None + + +def files_dir_from_data_path(data_path: Path) -> Path: + """MT5データフォルダまたはMQL5フォルダからFilesパスを作る。""" + resolved = data_path.resolve() + if resolved.name.lower() == "mql5": + return resolved / "Files" + + return resolved / "MQL5" / "Files" + + def terminal_files_dir(settings: Mt5PathSettings | None = None) -> Path: - """MT5のMQL5/Filesディレクトリを返す。""" + """MT5のMQL5/Filesディレクトリを返す。 + + 既定では `MQL5/python_for_ea` 配置から親の `MQL5/Files` を発見する。 + テストや特殊運用では `MT5_FILES_DIR` または `MT5_DATA_PATH` で上書きできる。 + """ + if settings and settings.files_dir is not None: + return settings.files_dir + + env_files_dir = os.getenv(ENV_MT5_FILES_DIR) + if env_files_dir: + return Path(env_files_dir) + + if settings and settings.data_path is not None: + return files_dir_from_data_path(settings.data_path) + + env_data_path = os.getenv(ENV_MT5_DATA_PATH) + if env_data_path: + return files_dir_from_data_path(Path(env_data_path)) + mt5_settings = settings or Mt5PathSettings() + if mt5_settings.user_name is not None or mt5_settings.terminal_id is not None: + user_name = mt5_settings.user_name or DEFAULT_USER_NAME + terminal_id = mt5_settings.terminal_id or DEFAULT_TERMINAL_ID + return ( + Path("C:/Users") + / user_name + / "AppData" + / "Roaming" + / "MetaQuotes" + / "Terminal" + / terminal_id + / "MQL5" + / "Files" + ) + + discovered_mql5_dir = discover_mql5_dir() + if discovered_mql5_dir is not None: + return discovered_mql5_dir / "Files" + + user_name = mt5_settings.user_name or DEFAULT_USER_NAME + terminal_id = mt5_settings.terminal_id or DEFAULT_TERMINAL_ID return ( Path("C:/Users") - / mt5_settings.user_name + / user_name / "AppData" / "Roaming" / "MetaQuotes" / "Terminal" - / mt5_settings.terminal_id + / terminal_id / "MQL5" / "Files" ) diff --git a/python_for_ea/tests/unit/test_paths.py b/python_for_ea/tests/unit/test_paths.py new file mode 100644 index 0000000..cf88647 --- /dev/null +++ b/python_for_ea/tests/unit/test_paths.py @@ -0,0 +1,91 @@ +"""MT5 path discovery tests.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ea_py.paths import ( + ENV_MT5_DATA_PATH, + ENV_MT5_FILES_DIR, + Mt5PathSettings, + discover_mql5_dir, + files_dir_from_data_path, + terminal_files_dir, +) + + +def test_discover_mql5_dir_from_nested_python_project(tmp_path: Path) -> None: + """MQL5/python_for_ea 配置から親の MQL5 ディレクトリを見つける。""" + nested = tmp_path / "Terminal" / "ABC" / "MQL5" / "python_for_ea" / "src" / "ea_py" / "paths.py" + nested.parent.mkdir(parents=True) + nested.write_text("", encoding="utf-8") + + actual = discover_mql5_dir(nested) + + assert actual == tmp_path / "Terminal" / "ABC" / "MQL5" + + +def test_terminal_files_dir_accepts_explicit_files_dir(tmp_path: Path) -> None: + """Tests and special deployments can pass the Files directory directly.""" + expected = tmp_path / "Files" + + actual = terminal_files_dir(Mt5PathSettings(files_dir=expected)) + + assert actual == expected + + +def test_terminal_files_dir_prefers_environment_files_dir( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """MT5_FILES_DIR overrides automatic discovery.""" + expected = tmp_path / "CustomFiles" + monkeypatch.setenv(ENV_MT5_FILES_DIR, str(expected)) + + actual = terminal_files_dir() + + assert actual == expected + + +def test_terminal_files_dir_accepts_legacy_user_and_terminal_settings() -> None: + """Existing tests and tools can still pass user and terminal settings.""" + settings = Mt5PathSettings(user_name="alice", terminal_id="TERMINAL123") + + actual = terminal_files_dir(settings) + + assert actual == Path( + "C:/Users/alice/AppData/Roaming/MetaQuotes/Terminal/TERMINAL123/MQL5/Files" + ) + + +def test_files_dir_from_data_path_accepts_terminal_data_path(tmp_path: Path) -> None: + """Terminal data paths are converted to MQL5/Files.""" + data_path = tmp_path / "Terminal" / "ABC" + + actual = files_dir_from_data_path(data_path) + + assert actual == data_path / "MQL5" / "Files" + + +def test_files_dir_from_data_path_accepts_mql5_path(tmp_path: Path) -> None: + """MQL5 paths are converted to their adjacent Files directory.""" + mql5_path = tmp_path / "Terminal" / "ABC" / "MQL5" + + actual = files_dir_from_data_path(mql5_path) + + assert actual == mql5_path / "Files" + + +def test_terminal_files_dir_accepts_environment_data_path( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """MT5_DATA_PATH can point at the terminal data folder.""" + data_path = tmp_path / "Terminal" / "ABC" + monkeypatch.setenv(ENV_MT5_DATA_PATH, str(data_path)) + + actual = terminal_files_dir() + + assert actual == data_path / "MQL5" / "Files"