Relocate Python EA helpers under MQL5
This commit is contained in:
@@ -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:
|
||||
// <TerminalDataPath>\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");
|
||||
|
||||
|
||||
@@ -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 <MyLib/Trading/SLTPManager.mqh>
|
||||
#include <MyLib/Panel/SLTPManagerPanel.mqh>
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| バッチファイル実行用の設定
|
||||
//+------------------------------------------------------------------+
|
||||
|
||||
// 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 <MyLib/Common/HITRuntimeController.mqh>
|
||||
#include <MyLib/Signals/HITEntrySignal.mqh>
|
||||
#include <MyLib/Common/HITExternalProcess.mqh>
|
||||
#include <MyLib/Signals/HITPythonSignalGateway.mqh>
|
||||
#include <MyLib/Trading/HITTradeManager.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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: `<TerminalDataPath>\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: `<TerminalDataPath>\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.
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
- Python出力: `trend_state.txt`
|
||||
- 完了フラグ: `process_done_trend.txt`
|
||||
- 実行中フラグ: `process_running_trend.txt`
|
||||
- 起動バッチ: `C:\ea_py\bat\get_trend_reply.bat`
|
||||
- 起動バッチ: `<TerminalDataPath>\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`
|
||||
- 起動バッチ: `<TerminalDataPath>\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` を作成する仕様にした。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ Python 側は注文送信を行いません。発注、注文管理、M15 確定
|
||||
MT5 EA からは `bat/` 経由でルート直下の互換スクリプトを起動します。
|
||||
|
||||
```text
|
||||
C:/ea_py/
|
||||
<TerminalDataPath>/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/
|
||||
<TerminalDataPath>/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
|
||||
|
||||
@@ -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%"
|
||||
|
||||
|
||||
@@ -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%"
|
||||
|
||||
|
||||
@@ -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/
|
||||
<TerminalDataPath>/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/
|
||||
<TerminalDataPath>/MQL5/python_for_ea/
|
||||
├─ AGENTS.md
|
||||
├─ README.md
|
||||
├─ pyproject.toml
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user