Merge pull request #2 from HirorihirorihK/codex/20260530_api

Codex/20260530 api
This commit is contained in:
HiroK
2026-06-04 20:12:20 +09:00
committed by GitHub
28 changed files with 5584 additions and 195 deletions
+19 -8
View File
@@ -100,7 +100,7 @@ enum SplitLotMode
input double lot_size = 0.01; // ロット数
input double spread_limit = 60; // 許容スプレッド(point)
input int magic_number = 10001; // マジックナンバー
input int initial_order = 0; // 起動時の注文(0:なし, 1:あり)
input int initial_order = 0; // 互換性維持用。起動時H4/H1再構築は常時実行
input int input_position_limit = 10; // 自EAの未約定注文+ポジション上限
input bool use_split_entry_zone = false; // H1予測ゾーンで分割エントリーする
input int split_entry_count = 3; // 分割エントリー本数(1..10)
@@ -108,6 +108,7 @@ input SplitLotMode split_lot_mode = SPLIT_LOT_TOTAL; // 分割ロット配分モ
input double split_total_lot_size = 0.09; // 総量分割モードの合計ロット
input double split_fixed_lot_size = 0.01; // 固定ロットモードの1注文ロット
input bool cancel_old_split_pending_on_new_zone = true; // 新ゾーン読込時に旧分割pendingを取消
input int input_cancel_retry_cooldown_seconds = 60; // pending取消失敗時のticket単位再試行間隔
input bool use_m15_entry_filter = true; // M15確定足で発注タイミングを確認
input double m15_entry_zone_atr_multiplier = 1.50; // M15平均レンジ何本分まで候補価格への接近を許可
input bool use_m15_imbalance_confirmation = true; // T1/T3でM15初動確認を追加
@@ -192,8 +193,8 @@ struct EAState
EAState g_ea; // EA全体の状態をここで管理
// initial_order 用の初回処理フラグ
// H4トレンド処理とH1エントリー処理で別々に管理し、初回起動時の再実行ループを防ぐ。
// Startup refresh flags for H4 trend and H1 entry candidates.
// Keep them separate to prevent repeated startup launches.
bool g_init_trend_pending = false;
bool g_init_entry_pending = false;
@@ -204,6 +205,10 @@ int g_pre_bars_M15 = 0;
bool g_bars_H1_check = false;
bool g_bars_M15_check = false;
// pending取消失敗時の過剰な取引サーバー要求を防止する。
ulong g_cancel_retry_tickets[];
datetime g_cancel_retry_at[];
// ティックごとの価格情報
struct TickContext
{
@@ -379,9 +384,9 @@ void ConfigurePythonGatewayPaths()
/**
* @brief EA起動時の初期化処理を行います。
*
* Python連携用のdone/runningファイル状態を確認し、`initial_order` が有効な場合は
* Python連携用のdone/runningファイル状態を確認し、起動直後は安全側として
* H4トレンド判定とH1エントリー価格生成をそれぞれ初回実行対象にします。
* H4/H1の初回フラグを分けることで、起動直後にH4処理だけが繰り返される状態を防ぎます。
* H4結果を再読込するまでは技術エラー停止値を維持し、古い連携結果での発注を防ぎます。
*/
int OnInit()
// プロセス完了ファイルと実行中ファイルの状態を初期化
@@ -391,9 +396,15 @@ int OnInit()
PrepareDoneFileOnInit(done_trend_file, running_trend_file, "trend");
PrepareDoneFileOnInit(done_entry_file, running_entry_file, "entry");
// initial_order=1 の場合でも、H4/H1を別々に1回だけ初回実行する。
g_init_trend_pending = (initial_order == 1);
g_init_entry_pending = (initial_order == 1);
// Always rebuild startup state before allowing new orders.
g_ea.trend_state = MARKET_TECHNICAL_ERROR_STOP;
g_ea.res_chk = 0;
g_ea.zone_res_chk = 0;
g_ea.zone_candidate_id = "0";
g_init_trend_pending = true;
g_init_entry_pending = true;
ArrayResize(g_cancel_retry_tickets, 0);
ArrayResize(g_cancel_retry_at, 0);
if(!InitializeSLTPManager())
return INIT_FAILED;
@@ -0,0 +1,609 @@
//+------------------------------------------------------------------+
//| HIT_EA.mq5 |
//| Copyright 2026, nanpin-martin.com |
//| https://www.nanpin-martin.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, nanpin-martin.com"
#property link "https://nanpin-martin.com/"
#property version "1.01"
#include <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
#define TASKKILL_WAIT_MILLISECONDS 5000
struct STARTUPINFO_W
{
uint cb;
string lpReserved;
string lpDesktop;
string lpTitle;
uint dwX;
uint dwY;
uint dwXSize;
uint dwYSize;
uint dwXCountChars;
uint dwYCountChars;
uint dwFillAttribute;
uint dwFlags;
ushort wShowWindow;
ushort cbReserved2;
long lpReserved2;
long hStdInput;
long hStdOutput;
long hStdError;
};
struct PROCESS_INFORMATION
{
long hProcess;
long hThread;
uint dwProcessId;
uint dwThreadId;
};
#import "kernel32.dll"
int CreateProcessW(string lpApplicationName, string lpCommandLine, long lpProcessAttributes, long lpThreadAttributes, int bInheritHandles, uint dwCreationFlags, long lpEnvironment, string lpCurrentDirectory, STARTUPINFO_W &lpStartupInfo, PROCESS_INFORMATION &lpProcessInformation);
int WaitForSingleObject(long hHandle, uint dwMilliseconds);
int GetExitCodeProcess(long hProcess, uint &lpExitCode);
int CloseHandle(long hObject);
long OpenProcess(uint dwDesiredAccess, int bInheritHandle, uint dwProcessId);
#import
// バッチファイルのパス
// Python helper scripts are managed under this MT5 data folder:
// <TerminalDataPath>\MQL5\python_for_ea
string python_app_dir = "";
string get_trend_reply_bat = ""; // H4: trend
string get_entry_reply_bat = ""; // H1: entry
string mt5_file_prefix = ""; // Python linkage file namespace
// プロセス完了ファイルの設定
string done_trend_file = "process_done_trend.txt";
string done_entry_file = "process_done_entry.txt";
// Python出力ファイル
string ohlc_h4_file = "ohlc_H4.csv";
string ohlc_h1_file = "ohlc_H1.csv";
string trend_state_file = "trend_state.txt";
string target_prices_file = "target_prices.txt";
string target_zones_file = "target_zones.txt";
// Python実行中を判定するための管理ファイル
string running_trend_file = "process_running_trend.txt";
string running_entry_file = "process_running_entry.txt";
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| インプットの設定
//+------------------------------------------------------------------+
enum SplitLotMode
{
SPLIT_LOT_TOTAL = 0, // 総ロットをN分割
SPLIT_LOT_FIXED = 1 // 1注文あたり固定ロット
};
//--- 入力パラメータ
input double lot_size = 0.01; // ロット数
input double spread_limit = 60; // 許容スプレッド(point)
input int magic_number = 10001; // マジックナンバー
input int initial_order = 0; // 起動時の注文(0:なし, 1:あり)
input int input_position_limit = 10; // 自EAの未約定注文+ポジション上限
input bool use_split_entry_zone = false; // H1予測ゾーンで分割エントリーする
input int split_entry_count = 3; // 分割エントリー本数(1..10)
input SplitLotMode split_lot_mode = SPLIT_LOT_TOTAL; // 分割ロット配分モード
input double split_total_lot_size = 0.09; // 総量分割モードの合計ロット
input double split_fixed_lot_size = 0.01; // 固定ロットモードの1注文ロット
input bool cancel_old_split_pending_on_new_zone = true; // 新ゾーン読込時に旧分割pendingを取消
input bool use_m15_entry_filter = true; // M15確定足で発注タイミングを確認
input double m15_entry_zone_atr_multiplier = 1.50; // M15平均レンジ何本分まで候補価格への接近を許可
input bool use_m15_imbalance_confirmation = true; // T1/T3でM15初動確認を追加
input int m15_imbalance_avg_body_period = 20; // M15平均実体計算期間
input double m15_imbalance_sensitivity = 2.0; // M15初動検知感度
input double m15_imbalance_min_avg_body_points = 1.0; // 平均実体の最小値(point)
input bool use_m15_imbalance_debug_log = false; // M15初動確認ログ
input int input_entry_max_candidate_age_minutes = 120; // H1候補価格を新規発注に使う最大経過分数
input bool input_sltp_manager_enabled = false; // UI連動SLTP管理を有効化
input bool input_sltp_show_panel = true; // SLTP操作パネルを表示
input bool input_sltp_use_breakeven = false; // 通常ブレークイーブンを有効化
input double input_sltp_breakeven_trigger_pips = 30.0; // 通常BE開始pips
input double input_sltp_breakeven_buffer_pips = 3.0; // 通常BE固定バッファpips
input bool input_sltp_use_elapsed_breakeven = false; // 保有時間BEを有効化
input double input_sltp_elapsed_breakeven_hours = 4.0; // 保有時間BE開始時間
input double input_sltp_elapsed_breakeven_buffer_pips = 3.0; // 保有時間BE固定バッファpips
input bool input_sltp_use_active_trailing = false; // アクティブトレーリングを有効化
input double input_sltp_active_breakeven_pips = 30.0; // アクティブ開始pips
input double input_sltp_active_stop_loss_offset_pips = 5.0; // 初期固定pips
input double input_sltp_active_step_trigger_pips = 10.0; // ステップ更新間隔pips
input double input_sltp_active_step_move_pips = 5.0; // 1ステップ固定追加pips
input bool input_sltp_use_tp_progress_stop = false; // TP進捗SLを有効化
input double input_sltp_tp_progress_trigger_percent = 70.0; // TP進捗SL開始率
input double input_sltp_tp_progress_sl_lock_percent = 30.0; // TP距離固定率
input bool input_sltp_use_high_volatility_limit = false; // 急変時SL引き締めを有効化
ulong slippage = 10; // スリッページ
//--- 主要な定数・設定
#define POSITION_LIMIT 48
#define ENTRY_H1_LIMIT 2 // H1本数(=2時間)
#define CLOSE_H1_LIMIT 12 // H1本数(=12時間)
#define HISTORY_BARS 72
#define OHLC_START_SHIFT 1 // 1: 確定足のみをPythonへ渡す
#define M15_CONFIRM_BARS 30
#define M15_MIN_ENTRY_ZONE_POINTS 10
#define M15_MIN_BODY_RATIO 0.25
#define M15_REJECTION_WICK_RATIO 0.35
#define ENTRY_RETRY_SECONDS 60
#define ENTRY_RETRY_LIMIT 10
// ANALYZE_TIMEFRAME は削除(H4/H1 両方使うため定数ではなく直接指定)
#define TARGET_SIZE 13
#define TARGET_ZONE_SCHEMA_VERSION 2
#define DEFAULT_TARGET_PRICE 0.0
#define PYTHON_TIMEOUT_SECONDS 600 // Python完了待ちの上限(秒)
#define MARKET_LOW_VOL_RANGE 0
#define MARKET_HIGH_VOL_RANGE 1
#define MARKET_LOW_VOL_UP 2
#define MARKET_HIGH_VOL_UP 3
#define MARKET_LOW_VOL_DOWN 4
#define MARKET_HIGH_VOL_DOWN 5
#define MARKET_TECHNICAL_ERROR_STOP 6
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| EAの状態をまとめる構造体
//+------------------------------------------------------------------+
struct EAState
{
int trend_state; // H4 market_state0..5、6は技術エラー停止)を格納する変数
int res_chk; // GPTの回答が正しく取得できたか
double en_price[5]; // エントリー基準価格
double tp_price[5]; // 利益確定価格
double sl_price[5]; // ロスカット基準価格
int zone_res_chk; // 予測ゾーンが正しく取得できたか
string zone_candidate_id; // H1確定足由来の候補ID
double zone_low[5]; // 分割エントリー用ゾーン下限
double zone_high[5]; // 分割エントリー用ゾーン上限
double zone_tp[5]; // 分割エントリー用TP
double zone_sl[5]; // 分割エントリー用SL
bool load_trend_flg; // ★変更:トレンドを更新するタイミングか
bool load_target_flg; // ★変更:ターゲット価格を更新するタイミングか
int chk_cnt; // エントリー判定の試行回数
datetime last_trend_update; // ★追加:前回トレンドを更新した時刻
datetime last_target_update; // ★変更:前回ターゲット価格を更新した時刻
datetime target_loaded_at; // H1候補価格をEAへ読み込んだサーバー時刻
datetime target_candidate_at; // H1候補価格の元になった確定足時刻
datetime last_chk;
};
EAState g_ea; // EA全体の状態をここで管理
// initial_order 用の初回処理フラグ
// H4トレンド処理とH1エントリー処理で別々に管理し、初回起動時の再実行ループを防ぐ。
bool g_init_trend_pending = false;
bool g_init_entry_pending = false;
// H4/H1のバー更新状態
int g_pre_bars_H4 = 0;
int g_pre_bars_H1 = 0;
int g_pre_bars_M15 = 0;
bool g_bars_H1_check = false;
bool g_bars_M15_check = false;
// ティックごとの価格情報
struct TickContext
{
double ask;
double bid;
double spread;
double spread_points;
int digits;
};
// 外部Pythonプロセスの状態
struct ExternalProcessState
{
long handle;
uint process_id;
bool active;
bool exit_code_ready;
uint exit_code;
datetime started_at;
};
ExternalProcessState g_trend_process;
ExternalProcessState g_entry_process;
CSLTPManager g_sltp_manager;
CSLTPManagerPanel g_sltp_panel;
SLTPManagerPanelSettings g_sltp_settings;
bool g_sltp_settings_valid = false;
bool g_sltp_panel_created = false;
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| Python連携パスをMT5データフォルダから組み立てる関数
//+------------------------------------------------------------------+
/**
* @brief MT5データフォルダ配下のMQL5ルートを返します。
*/
string MQL5RootPath()
{
return TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5";
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief EAと同じMQL5ツリー内にあるPython補助アプリのルートを返します。
*/
string PythonAppDir()
{
return MQL5RootPath() + "\\python_for_ea";
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief Python補助アプリ配下のbatファイル絶対パスを返します。
*/
string PythonBatchPath(const string filename)
{
return PythonAppDir() + "\\bat\\" + filename;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief ファイル名に使える安全なトークン文字か判定します。
*/
bool IsSafeFileTokenChar(const int ch)
{
return ((ch >= 48 && ch <= 57) || // 0-9
(ch >= 65 && ch <= 90) || // A-Z
(ch >= 97 && ch <= 122) || // a-z
ch == 95 || ch == 45); // _ or -
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief シンボル名などをファイル名向けの安全なトークンへ変換します。
*/
string SanitizeFileToken(const string value)
{
string result = "";
int length = StringLen(value);
for(int i = 0; i < length; i++)
{
int ch = StringGetCharacter(value, i);
if(IsSafeFileTokenChar(ch))
result += StringSubstr(value, i, 1);
else
result += "_";
}
if(result == "")
return "UNKNOWN";
return result;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief 同一Terminal内の複数EA/複数シンボルで連携ファイルが衝突しない接頭辞を返します。
*/
string BuildMT5FilePrefix()
{
return "HIT_" + SanitizeFileToken(_Symbol) + "_" + IntegerToString(magic_number);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief Python連携ファイル名へEA固有の接頭辞を付けます。
*/
string PrefixedMT5FileName(const string base_name)
{
if(mt5_file_prefix == "")
return base_name;
return mt5_file_prefix + "_" + base_name;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief Python連携で使用するMT5 Files配下のファイル名を初期化します。
*/
void ConfigurePythonGatewayFileNames()
{
mt5_file_prefix = BuildMT5FilePrefix();
ohlc_h4_file = PrefixedMT5FileName("ohlc_H4.csv");
ohlc_h1_file = PrefixedMT5FileName("ohlc_H1.csv");
done_trend_file = PrefixedMT5FileName("process_done_trend.txt");
done_entry_file = PrefixedMT5FileName("process_done_entry.txt");
trend_state_file = PrefixedMT5FileName("trend_state.txt");
target_prices_file = PrefixedMT5FileName("target_prices.txt");
target_zones_file = PrefixedMT5FileName("target_zones.txt");
running_trend_file = PrefixedMT5FileName("process_running_trend.txt");
running_entry_file = PrefixedMT5FileName("process_running_entry.txt");
Print("Python file prefix: ", mt5_file_prefix);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief Python連携で使用するbatパスを初期化します。
*/
void ConfigurePythonGatewayPaths()
{
ConfigurePythonGatewayFileNames();
python_app_dir = PythonAppDir();
get_trend_reply_bat = PythonBatchPath("get_trend_reply.bat");
get_entry_reply_bat = PythonBatchPath("get_entry_reply.bat");
Print("Python app dir: ", python_app_dir);
}
//+------------------------------------------------------------------+
//| 起動時の処理
//+------------------------------------------------------------------+
/**
* @brief EA起動時の初期化処理を行います。
*
* Python連携用のdone/runningファイル状態を確認し、`initial_order` が有効な場合は
* H4トレンド判定とH1エントリー価格生成をそれぞれ初回実行対象にします。
* H4/H1の初回フラグを分けることで、起動直後にH4処理だけが繰り返される状態を防ぎます。
*/
int OnInit()
// プロセス完了ファイルと実行中ファイルの状態を初期化
{
ConfigurePythonGatewayPaths();
PrepareDoneFileOnInit(done_trend_file, running_trend_file, "trend");
PrepareDoneFileOnInit(done_entry_file, running_entry_file, "entry");
// initial_order=1 の場合でも、H4/H1を別々に1回だけ初回実行する。
g_init_trend_pending = (initial_order == 1);
g_init_entry_pending = (initial_order == 1);
if(!InitializeSLTPManager())
return INIT_FAILED;
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| 終了時の処理
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
ResetExternalProcessState(g_trend_process);
ResetExternalProcessState(g_entry_process);
if(g_sltp_panel_created)
g_sltp_panel.Destroy(reason);
// Clear chart status text left by Comment() when the EA is removed.
Comment("");
ChartRedraw(0);
}
//+------------------------------------------------------------------+
//| ティック毎の処理
//+------------------------------------------------------------------+
/**
* @brief ティック受信ごとにEA全体の処理を制御します。
*
* 処理順序は、ティック情報取得、期限切れ注文/ポジション管理、H4トレンド更新、
* H1エントリー価格更新、M15エントリータイミング更新、ステータス表示、新規注文判定の順です。
* 時間制限処理はPython完了待ちやスプレッド判定より前に実行します。
*/
void OnTick()
{
TickContext ctx;
if(!GetTickContext(ctx))
return;
// 期限切れ注文・ポジションは、Python待ちやスプレッド制限に関係なく先に処理する。
ManageExpiredTrades();
// Existing-position protection stays active even while Python results are pending.
ManageSLTPPositions();
// 外部Pythonがdoneを返さない場合に、一定時間で待機状態を解除して再実行対象にする。
RecoverTimedOutPythonProcesses();
// H4新バーまたは初回起動時に、トレンド判定用Pythonを起動する。
ProcessTrendUpdate(g_ea);
// トレンド判定が完了していない場合、新規注文側の処理だけをスキップする。
if(!IsTrendResultReady())
return;
RefreshTrendState(g_ea);
// H1新バーまたは初回起動時に、エントリー価格生成用Pythonを起動する。
ProcessEntryUpdate(g_ea);
// M15確定足ごとに、H1候補価格を発注してよいタイミングか再判定する。
ProcessM15EntryTimingUpdate();
UpdateStatusComment(ctx);
// ここから下は新規注文に関する処理。
if(!IsSpreadAllowed(ctx))
return;
if(!IsEntryResultReady())
return;
RefreshTargetPrices(g_ea);
ProcessEntryDecisionIfNeeded(g_ea, ctx);
}
//+------------------------------------------------------------------+
//| SLTP manager panel and settings bridge
//+------------------------------------------------------------------+
string SLTPBoolText(const bool value)
{
return value ? "ON" : "OFF";
}
void LoadSLTPInputSettings(SLTPManagerPanelSettings &settings)
{
settings.manager_enabled = input_sltp_manager_enabled;
settings.use_breakeven = input_sltp_use_breakeven;
settings.breakeven_trigger_pips = input_sltp_breakeven_trigger_pips;
settings.breakeven_buffer_pips = input_sltp_breakeven_buffer_pips;
settings.use_elapsed_breakeven = input_sltp_use_elapsed_breakeven;
settings.elapsed_breakeven_hours = input_sltp_elapsed_breakeven_hours;
settings.elapsed_breakeven_buffer_pips = input_sltp_elapsed_breakeven_buffer_pips;
settings.use_active_trailing = input_sltp_use_active_trailing;
settings.active_breakeven_pips = input_sltp_active_breakeven_pips;
settings.active_stop_loss_offset_pips = input_sltp_active_stop_loss_offset_pips;
settings.active_step_trigger_pips = input_sltp_active_step_trigger_pips;
settings.active_step_move_pips = input_sltp_active_step_move_pips;
settings.use_tp_progress_stop = input_sltp_use_tp_progress_stop;
settings.tp_progress_trigger_percent = input_sltp_tp_progress_trigger_percent;
settings.tp_progress_sl_lock_percent = input_sltp_tp_progress_sl_lock_percent;
settings.use_high_volatility_limit = input_sltp_use_high_volatility_limit;
}
bool ApplySLTPSettings(const SLTPManagerPanelSettings &settings,
const bool print_summary)
{
g_sltp_settings_valid = false;
g_sltp_manager.SetMagicNumber((ulong)magic_number);
g_sltp_manager.SetSymbol(_Symbol);
g_sltp_manager.SetDeviationInPoints(slippage);
g_sltp_manager.SetBreakevenSettings(settings.use_breakeven,
settings.breakeven_trigger_pips,
settings.breakeven_buffer_pips);
g_sltp_manager.SetElapsedBreakevenSettings(settings.use_elapsed_breakeven,
settings.elapsed_breakeven_hours,
settings.elapsed_breakeven_buffer_pips);
g_sltp_manager.SetActiveTrailingSettings(settings.use_active_trailing,
settings.active_breakeven_pips,
settings.active_stop_loss_offset_pips,
settings.active_step_trigger_pips,
settings.active_step_move_pips);
g_sltp_manager.SetTpProgressStopSettings(settings.use_tp_progress_stop,
settings.tp_progress_trigger_percent,
settings.tp_progress_sl_lock_percent);
g_sltp_manager.SetHighVolatilityLimitSettings(settings.use_high_volatility_limit);
if(!g_sltp_manager.ValidateSettings())
{
Print("HIT SLTP settings rejected. Manager is disabled until valid settings are applied.");
return false;
}
g_sltp_settings = settings;
g_sltp_settings_valid = true;
if(print_summary)
{
Print("HIT SLTP settings: Manager=", SLTPBoolText(settings.manager_enabled),
" BE=", SLTPBoolText(settings.use_breakeven),
" ElapsedBE=", SLTPBoolText(settings.use_elapsed_breakeven),
" ActiveTrail=", SLTPBoolText(settings.use_active_trailing),
" TPProgress=", SLTPBoolText(settings.use_tp_progress_stop),
" HighVol=", SLTPBoolText(settings.use_high_volatility_limit));
}
return true;
}
bool InitializeSLTPManager()
{
SLTPManagerPanelSettings initial_settings;
LoadSLTPInputSettings(initial_settings);
if(!ApplySLTPSettings(initial_settings, true))
return false;
g_sltp_panel.Init(g_sltp_settings);
if(input_sltp_show_panel)
{
if(!g_sltp_panel.CreatePanel())
{
Print("HIT SLTP panel is unavailable. EA continues with input-based SLTP settings.");
return true;
}
g_sltp_panel.SetInitialValues();
g_sltp_panel_created = true;
}
return true;
}
void ManageSLTPPositions()
{
if(!g_sltp_settings_valid)
return;
if(!g_sltp_settings.manager_enabled)
return;
g_sltp_manager.ManagePositions();
g_sltp_manager.HighVolatilityLimit();
}
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(!g_sltp_panel_created)
return;
SLTPManagerPanelSettings candidate_settings = g_sltp_settings;
if(g_sltp_panel.HandleChartEvent(id, lparam, dparam, sparam, candidate_settings))
{
if(!ApplySLTPSettings(candidate_settings, true))
Print("HIT SLTP panel apply failed. Please review panel values.");
}
}
// Function implementations are kept in project headers so the EA entry points stay compact.
#include <MyLib/Common/HITRuntimeController.mqh>
#include <MyLib/Signals/HITEntrySignal.mqh>
#include <MyLib/Common/HITExternalProcess.mqh>
#include <MyLib/Signals/HITPythonSignalGateway.mqh>
#include <MyLib/Trading/HITTradeManager.mqh>
@@ -36,6 +36,8 @@ bool GetTickContext(TickContext &ctx)
*/
void ManageExpiredTrades()
{
CleanupPendingOrderCancelAttempts();
if(OrdersTotal() > 0)
CancelExpiredOrders();
@@ -0,0 +1,289 @@
#ifndef HIT_RUNTIME_CONTROLLER_MQH
#define HIT_RUNTIME_CONTROLLER_MQH
//+------------------------------------------------------------------+
//| ティック情報を取得する関数
//+------------------------------------------------------------------+
/**
* @brief 現在のAsk/Bid/スプレッド情報を取得してTickContextへ格納します。
*
* @param ctx 取得したティック情報を格納する構造体参照。
* @return 取得成功時はtrue、ティック情報を取得できない場合はfalse。
*/
bool GetTickContext(TickContext &ctx)
{
MqlTick last_tick;
if(!SymbolInfoTick(_Symbol, last_tick))
return false;
ctx.ask = last_tick.ask;
ctx.bid = last_tick.bid;
ctx.spread_points = MathRound((ctx.ask - ctx.bid) / Point());
ctx.spread = ctx.spread_points * Point();
ctx.digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
return true;
}
//+------------------------------------------------------------------+
//| 期限切れ注文・期限切れポジションを処理する関数
//+------------------------------------------------------------------+
/**
* @brief 期限切れの未約定注文と保有ポジションを処理します。
*
* 未約定注文はENTRY_H1_LIMIT時間、保有ポジションはCLOSE_H1_LIMIT時間を基準に判定します。
* 新規注文条件とは独立して、OnTickの早い段階で実行される想定です。
*/
void ManageExpiredTrades()
{
if(OrdersTotal() > 0)
CancelExpiredOrders();
if(PositionsTotal() > 0)
CloseExpiredPositions();
}
//+------------------------------------------------------------------+
//| H4更新検知とトレンド判定Python起動を処理する関数
//+------------------------------------------------------------------+
/**
* @brief H4新バーまたは初回起動を検知し、トレンド判定Pythonを起動します。
*
* @param state EA全体の状態。トレンド更新開始フラグを更新します。
*
* Python処理中はdoneファイルが存在しないため、CSVの上書きと二重起動を抑止します。
*/
void ProcessTrendUpdate(EAState &state)
{
int current_bars_H4 = iBars(NULL, PERIOD_H4);
if(g_pre_bars_H4 == 0)
g_pre_bars_H4 = current_bars_H4;
int bars_H4_change = current_bars_H4 - g_pre_bars_H4;
bool trend_trigger = (bars_H4_change > 0 || g_init_trend_pending);
if(!trend_trigger)
return;
if(RecordOHLCAndExecuteBatch_Trend(state))
{
g_init_trend_pending = false;
g_pre_bars_H4 = current_bars_H4;
}
}
//+------------------------------------------------------------------+
//| トレンド判定Pythonの完了状態を返す関数
//+------------------------------------------------------------------+
/**
* @brief H4トレンド判定Pythonの完了状態を確認します。
*
* @return `process_done_trend.txt` が存在する場合はtrue、未完了の場合はfalse。
*/
bool IsTrendResultReady()
{
return IsProcessResultReady(done_trend_file, running_trend_file, trend_state_file, "trend", g_trend_process);
}
//+------------------------------------------------------------------+
//| トレンド判定結果をEA状態へ反映する関数
//+------------------------------------------------------------------+
/**
* @brief 完了済みのH4トレンド判定結果をEA状態へ反映します。
*
* @param state EA全体の状態。`trend_state` と最終更新時刻が更新されます。
*/
void RefreshTrendState(EAState &state)
{
GetTrendState(state);
}
//+------------------------------------------------------------------+
//| H1更新検知とエントリー価格生成Python起動を処理する関数
//+------------------------------------------------------------------+
/**
* @brief H1新バーまたは初回起動を検知し、エントリー価格生成Pythonを起動します。
*
* @param state EA全体の状態。ターゲット価格更新開始フラグや判定リトライ状態を更新します。
*
* Python処理中はdoneファイルが存在しないため、H1 CSVの上書きと二重起動を抑止します。
*/
void ProcessEntryUpdate(EAState &state)
{
int current_bars_H1 = iBars(NULL, PERIOD_H1);
if(g_pre_bars_H1 == 0)
g_pre_bars_H1 = current_bars_H1;
int bars_H1_change = current_bars_H1 - g_pre_bars_H1;
bool entry_trigger = (bars_H1_change > 0 || g_init_entry_pending);
if(!entry_trigger)
return;
if(RecordOHLCAndExecuteBatch_Entry(state))
{
g_init_entry_pending = false;
g_bars_H1_check = true;
state.chk_cnt = 0;
state.last_chk = 0;
g_pre_bars_H1 = current_bars_H1;
}
}
//+------------------------------------------------------------------+
//| M15更新検知とエントリータイミング判定トリガーを処理する関数
//+------------------------------------------------------------------+
/**
* @brief M15新バーを検知し、H1候補価格の発注判定を実行できる状態にします。
*
* M15は方向判定を上書きする足ではなく、H4/H1で決めた候補価格を発注する
* タイミング確認として使います。初回は直近の確定M15足で一度だけ判定可能にします。
*/
void ProcessM15EntryTimingUpdate()
{
int current_bars_M15 = iBars(NULL, PERIOD_M15);
if(current_bars_M15 <= 0)
return;
if(g_pre_bars_M15 == 0)
{
g_pre_bars_M15 = current_bars_M15;
g_bars_M15_check = true;
return;
}
int bars_M15_change = current_bars_M15 - g_pre_bars_M15;
if(bars_M15_change <= 0)
return;
g_bars_M15_check = true;
g_pre_bars_M15 = current_bars_M15;
}
//+------------------------------------------------------------------+
//| チャートコメント文字列を作成する関数
//+------------------------------------------------------------------+
/**
* @brief チャート左上に表示するステータスメッセージを組み立てます。
*
* @param ctx 現在のAsk/Bid/スプレッド情報。
* @return Comment()へ渡す表示用文字列。
*/
string BuildStatusMessage(TickContext &ctx)
{
string message = StringFormat(
" \nAsk: %." + IntegerToString(ctx.digits) + "f"
"\nBid: %." + IntegerToString(ctx.digits) + "f"
"\nSpread: %." + IntegerToString(ctx.digits) + "f"
"\nSpread Points: %.0f"
"\n\nLast Trend Update:\n %s"
"\n\nMarket State: %d (%s)"
"\n\nLast Target Update:\n %s"
"\n\nMy Used Count: %d / %d"
"\nSplit Zone: %s N=%d ZoneID=%s"
"\n\n[T1 Buy Stop ] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f"
"\n[T2 Buy Limit] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f"
"\n[T3 Sell Stop] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f"
"\n[T4 SellLimit] en: %." + IntegerToString(ctx.digits) + "f tp: %." + IntegerToString(ctx.digits) + "f sl: %." + IntegerToString(ctx.digits) + "f\n",
ctx.ask, ctx.bid, ctx.spread, ctx.spread_points,
TimeToString(g_ea.last_trend_update, TIME_DATE | TIME_MINUTES),
g_ea.trend_state, MarketStateName(g_ea.trend_state),
TimeToString(g_ea.last_target_update, TIME_DATE | TIME_MINUTES),
CountMyUsed(), EffectivePositionLimit(),
(use_split_entry_zone ? "ON" : "OFF"), EffectiveSplitEntryCount(), g_ea.zone_candidate_id,
g_ea.en_price[1], g_ea.tp_price[1], g_ea.sl_price[1],
g_ea.en_price[2], g_ea.tp_price[2], g_ea.sl_price[2],
g_ea.en_price[3], g_ea.tp_price[3], g_ea.sl_price[3],
g_ea.en_price[4], g_ea.tp_price[4], g_ea.sl_price[4]
);
return message;
}
//+------------------------------------------------------------------+
//| チャートコメントを更新する関数
//+------------------------------------------------------------------+
/**
* @brief チャート上のステータスコメントを更新します。
*
* @param ctx 現在のAsk/Bid/スプレッド情報。
*/
void UpdateStatusComment(TickContext &ctx)
{
Comment(BuildStatusMessage(ctx));
}
//+------------------------------------------------------------------+
//| market_stateの表示名を返す関数
//+------------------------------------------------------------------+
/**
* @brief H4 market_stateの人間可読名を返します。
*
* @param market_state Pythonが出力したH4環境分類(0..5、6は技術エラー停止)。
* @return 表示用ラベル。
*/
string MarketStateName(const int market_state)
{
switch(market_state)
{
case MARKET_LOW_VOL_RANGE:
return "LOW_VOL_RANGE";
case MARKET_HIGH_VOL_RANGE:
return "HIGH_VOL_RANGE";
case MARKET_LOW_VOL_UP:
return "LOW_VOL_UP";
case MARKET_HIGH_VOL_UP:
return "HIGH_VOL_UP";
case MARKET_LOW_VOL_DOWN:
return "LOW_VOL_DOWN";
case MARKET_HIGH_VOL_DOWN:
return "HIGH_VOL_DOWN";
case MARKET_TECHNICAL_ERROR_STOP:
return "TECHNICAL_ERROR_STOP";
default:
return "UNKNOWN";
}
}
//+------------------------------------------------------------------+
//| スプレッドが許容範囲内か判定する関数
//+------------------------------------------------------------------+
/**
* @brief 現在スプレッドが入力パラメータの許容範囲内か判定します。
*
* @param ctx 現在のスプレッド情報。
* @return 許容範囲内ならtrue、超過している場合はfalse。
*/
bool IsSpreadAllowed(TickContext &ctx)
{
return (ctx.spread <= spread_limit * Point());
}
//+------------------------------------------------------------------+
//| エントリー価格生成Pythonの完了状態を返す関数
//+------------------------------------------------------------------+
/**
* @brief H1エントリー価格生成Pythonの完了状態を確認します。
*
* @return `process_done_entry.txt` が存在する場合はtrue、未完了の場合はfalse。
*/
bool IsEntryResultReady()
{
return IsProcessResultReady(done_entry_file, running_entry_file, target_prices_file, "entry", g_entry_process);
}
//+------------------------------------------------------------------+
//| ターゲット価格をEA状態へ反映する関数
//+------------------------------------------------------------------+
/**
* @brief 完了済みのH1エントリー価格生成結果をEA状態へ反映します。
*
* @param state EA全体の状態。`res_chk` と各注文タイプのen/tp/slが更新されます。
*/
void RefreshTargetPrices(EAState &state)
{
GetTargetPrices(state);
}
//+------------------------------------------------------------------+
#endif
+23 -5
View File
@@ -198,6 +198,22 @@ string TargetCandidateAgeText(EAState &state)
return IntegerToString(age_seconds) + "s";
}
//+------------------------------------------------------------------+
//| pending注文コメントへ付与するH1候補IDを返す関数
//+------------------------------------------------------------------+
/**
* @brief 復元済みのH1確定足時刻がある場合だけ候補IDをコメントへ付与します。
*/
string TargetCandidateCommentSuffix(EAState &state)
{
if(state.target_candidate_at <= 0 ||
state.zone_candidate_id == "" ||
state.zone_candidate_id == "0")
return "";
return "Z" + state.zone_candidate_id;
}
//+------------------------------------------------------------------+
//| 許可された注文タイプだけを順番に送信する関数
//+------------------------------------------------------------------+
@@ -267,9 +283,10 @@ string TwoDigit(const int value)
//+------------------------------------------------------------------+
//| 分割slot識別子を作る関数
//+------------------------------------------------------------------+
string SplitSlotKey(const string candidate_id, const int slot_index, const int split_count)
string SplitSlotKey(const int orderType, const string candidate_id, const int slot_index, const int split_count)
{
return "Z" + candidate_id + "#" + TwoDigit(slot_index) + "/" + TwoDigit(split_count);
return "Z" + candidate_id + "#T" + IntegerToString(orderType) + "-" +
TwoDigit(slot_index) + "/" + TwoDigit(split_count);
}
//+------------------------------------------------------------------+
@@ -482,7 +499,7 @@ int TrySendSplitEntryOrders(const int orderType, EAState &state, TickContext &ct
break;
}
string slot_key = SplitSlotKey(state.zone_candidate_id, slot, split_count);
string slot_key = SplitSlotKey(orderType, state.zone_candidate_id, slot, split_count);
if(HasExistingSplitSlot(slot_key))
{
Print("[Split Skip] slot already exists. key=", slot_key);
@@ -503,7 +520,7 @@ int TrySendSplitEntryOrders(const int orderType, EAState &state, TickContext &ct
Print("[Split ", entry_type, " Order Try] key=", slot_key,
" en=", en, " tp=", tp, " sl=", sl, " volume=", volume);
if(SendOrder(orderType, en, tp, sl, volume, slot_key))
if(SendOrder(orderType, en, tp, sl, volume, slot_key, TargetCandidateReferenceTime(state)))
{
Print("[Split ", entry_type, " Order Sent] key=", slot_key,
" en=", en, " tp=", tp, " sl=", sl, " volume=", volume);
@@ -568,7 +585,8 @@ bool TrySendEntryOrder(const int orderType, EAState &state, TickContext &ctx)
Print("[", entry_type, " Order Try at ", cur_price, "] en=", en, " tp=", tp, " sl=", sl);
if(SendOrder(orderType, en, tp, sl, lot_size, ""))
if(SendOrder(orderType, en, tp, sl, lot_size,
TargetCandidateCommentSuffix(state), TargetCandidateReferenceTime(state)))
{
Print("[", entry_type, " Order Sent] ticket ok. en=", en, " tp=", tp, " sl=", sl);
return true;
File diff suppressed because it is too large Load Diff
@@ -218,25 +218,23 @@ bool TryParseStrictDouble(const string value, double &parsed)
//| H1候補IDを安全な12桁時刻トークンへ正規化する関数
//+------------------------------------------------------------------+
/**
* @brief Pythonから受け取ったcandidate_idを注文コメントに使える数字だけへ丸めます。
* @brief Pythonから受け取ったcandidate_idが厳格な12桁数字の場合だけ採用します。
*/
string NormalizeTargetCandidateId(const string value)
{
string text = TrimText(value);
string output = "";
int length = StringLen(text);
for(int i = 0; i < length && StringLen(output) < 12; i++)
{
int ch = StringGetCharacter(text, i);
if(IsDecimalDigit(ch))
output += StringSubstr(text, i, 1);
}
if(StringLen(output) <= 0)
if(length != 12)
return "0";
return output;
for(int i = 0; i < length; i++)
{
int ch = StringGetCharacter(text, i);
if(!IsDecimalDigit(ch))
return "0";
}
return text;
}
//+------------------------------------------------------------------+
@@ -406,11 +404,13 @@ bool GetTrendState(EAState &state)
if(ProcessDone && g_ea.load_trend_flg)
{
int trend_state;
LoadTrendState(trend_state);
state.trend_state = trend_state;
Print("market_state: ", state.trend_state, " (", MarketStateName(state.trend_state), ")");
g_ea.load_trend_flg = false;
g_ea.last_trend_update = TimeLocal();
LoadTrendState(trend_state);
state.trend_state = trend_state;
Print("market_state: ", state.trend_state, " (", MarketStateName(state.trend_state), ")");
CancelPendingOrdersNotAllowedByTrend(state.trend_state);
g_init_entry_pending = true; // Rebuild H1 candidates from the refreshed H4 state.
g_ea.load_trend_flg = false;
g_ea.last_trend_update = TimeLocal();
}
return true;
}
@@ -685,6 +685,13 @@ void LoadTargetZones(EAState &state)
datetime candidate_at = 0;
if(TryParseTargetCandidateTime(state.zone_candidate_id, candidate_at))
state.target_candidate_at = candidate_at;
else if(state.zone_res_chk == 1)
{
Print("target_zones invalid candidate_id: ", line);
FileClose(filehandle);
ResetTargetZones(state);
return;
}
else
state.target_candidate_at = 0;
continue;
@@ -0,0 +1,725 @@
#ifndef HIT_PYTHON_SIGNAL_GATEWAY_MQH
#define HIT_PYTHON_SIGNAL_GATEWAY_MQH
//+------------------------------------------------------------------+
//| 最新のOHLCデータを取得する関数
//+------------------------------------------------------------------+
/**
* @brief 指定時間足のOHLCデータを取得します。
*
* @param times 取得したバー時刻を格納する配列。
* @param open_prices 取得したOpen価格を格納する配列。
* @param high_prices 取得したHigh価格を格納する配列。
* @param low_prices 取得したLow価格を格納する配列。
* @param close_prices 取得したClose価格を格納する配列。
* @param tf 取得対象の時間足。
* @param bars_count 取得するバー本数。
* @return 取得に成功した場合はtrue、失敗した場合はfalse。
*
* `OHLC_START_SHIFT=1` のため、形成中のバーではなく確定足から取得します。
*/
bool GetLatestOHLC(datetime &times[], double &open_prices[], double &high_prices[], double &low_prices[], double &close_prices[], ENUM_TIMEFRAMES tf, int bars_count)
{
ArrayResize(times, 0);
ArrayResize(open_prices, 0);
ArrayResize(high_prices, 0);
ArrayResize(low_prices, 0);
ArrayResize(close_prices, 0);
if(bars_count < 2)
return false;
// OHLCデータを取得
// CopyTime と CopyRates を別々に呼ばず、MqlRates の time/open/high/low/close を同一配列から取得する。
// OHLC_START_SHIFT=1 のため、形成中の0本目ではなく確定足からPythonへ渡す。
MqlRates rates[];
int copied = CopyRates(_Symbol, tf, OHLC_START_SHIFT, bars_count, rates);
if(copied <= 0)
{
Print(__FUNCTION__, ": Failed to copy rates data (bars=", bars_count, ") err=", GetLastError());
return false;
}
ArrayResize(times, copied);
ArrayResize(open_prices, copied);
ArrayResize(high_prices, copied);
ArrayResize(low_prices, copied);
ArrayResize(close_prices, copied);
// 必要なデータを配列に格納
for(int i = 0; i < copied; i++)
{
times[i] = rates[i].time;
open_prices[i] = rates[i].open;
high_prices[i] = rates[i].high;
low_prices[i] = rates[i].low;
close_prices[i] = rates[i].close;
}
return true;
}
//+------------------------------------------------------------------+
//| "ohlc.csv"を出力する関数
//+------------------------------------------------------------------+
/**
* @brief OHLC配列をPython入力用CSVとして出力します。
*
* @param filename 出力するCSVファイル名。
* @param times バー時刻配列。
* @param open_prices Open価格配列。
* @param high_prices High価格配列。
* @param low_prices Low価格配列。
* @param close_prices Close価格配列。
* @return CSV出力に成功した場合はtrue。
*/
bool RecordOHLC(const string filename, const datetime &times[], const double &open_prices[], const double &high_prices[], const double &low_prices[], const double &close_prices[])
{
// 出力先ファイル名
// string filename = "ohlc.csv";
// ファイルを "書き込みモード" でオープン (テキスト/ANSI)
int fileHandle = FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI);
// ファイルが開けたかチェック
if(fileHandle == INVALID_HANDLE)
{
Print(__FUNCTION__, " : Failed to open file: ", GetLastError());
return false;
}
// 配列サイズを取得 (times, open, high, low, close の要素数は同じ前提)
int size = ArraySize(times);
// ヘッダー行を追加
FileWrite(fileHandle, "Time,Open,High,Low,Close");
// 小数点の桁数を取得
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
// フォーマット文字列を動的に生成
string formatString = StringFormat("%%s,%%.%df,%%.%df,%%.%df,%%.%df", digits, digits, digits, digits);
// 1行ずつ "時刻, Open, High, Low, Close" の形式で書き込み
for(int i = 0; i < size; i++)
{
// 時間をフォーマット
string timeStr = TimeToString(times[i], TIME_DATE | TIME_MINUTES);
// 1行分の文字列を生成 (小数点以下の桁数を `digits` に調整)
string line = StringFormat(formatString,
timeStr, open_prices[i], high_prices[i], low_prices[i], close_prices[i]);
// ファイルに書き込み (改行付き)
FileWrite(fileHandle, line);
}
// 書き込み終了後、ファイルを閉じる
FileClose(fileHandle);
return true;
}
//+------------------------------------------------------------------+
//| 文字列の前後空白を除去する関数
//+------------------------------------------------------------------+
/**
* @brief Python連携ファイルから読んだ1行をパース前に正規化します。
*/
string TrimText(const string value)
{
string text = value;
StringTrimLeft(text);
StringTrimRight(text);
return text;
}
//+------------------------------------------------------------------+
//| 10進数字か判定する関数
//+------------------------------------------------------------------+
bool IsDecimalDigit(const int ch)
{
return (ch >= 48 && ch <= 57);
}
//+------------------------------------------------------------------+
//| 厳格な整数パース
//+------------------------------------------------------------------+
/**
* @brief 数字だけで構成された整数行を読み込みます。不正文字があればfalse。
*/
bool TryParseStrictInteger(const string value, int &parsed)
{
string text = TrimText(value);
int length = StringLen(text);
if(length <= 0)
return false;
for(int i = 0; i < length; i++)
{
if(!IsDecimalDigit(StringGetCharacter(text, i)))
return false;
}
parsed = (int)StringToInteger(text);
return true;
}
//+------------------------------------------------------------------+
//| 厳格な小数パース
//+------------------------------------------------------------------+
/**
* @brief 単純な10進数行を読み込みます。指数表記や余計な文字は許可しません。
*/
bool TryParseStrictDouble(const string value, double &parsed)
{
string text = TrimText(value);
int length = StringLen(text);
if(length <= 0)
return false;
bool has_digit = false;
bool has_dot = false;
int start_index = 0;
int first_char = StringGetCharacter(text, 0);
if(first_char == 43 || first_char == 45) // + or -
{
if(length == 1)
return false;
start_index = 1;
}
for(int i = start_index; i < length; i++)
{
int ch = StringGetCharacter(text, i);
if(IsDecimalDigit(ch))
{
has_digit = true;
continue;
}
if(ch == 46 && !has_dot) // dot
{
has_dot = true;
continue;
}
return false;
}
if(!has_digit)
return false;
parsed = StringToDouble(text);
return true;
}
//+------------------------------------------------------------------+
//| H1候補IDを安全な12桁時刻トークンへ正規化する関数
//+------------------------------------------------------------------+
/**
* @brief Pythonから受け取ったcandidate_idを注文コメントに使える数字だけへ丸めます。
*/
string NormalizeTargetCandidateId(const string value)
{
string text = TrimText(value);
string output = "";
int length = StringLen(text);
for(int i = 0; i < length && StringLen(output) < 12; i++)
{
int ch = StringGetCharacter(text, i);
if(IsDecimalDigit(ch))
output += StringSubstr(text, i, 1);
}
if(StringLen(output) <= 0)
return "0";
return output;
}
//+------------------------------------------------------------------+
//| H1候補IDから確定足時刻を復元する関数
//+------------------------------------------------------------------+
/**
* @brief `yyyyMMddHHmm` 形式のcandidate_idをdatetimeへ変換します。
*/
bool TryParseTargetCandidateTime(const string candidate_id, datetime &candidate_at)
{
candidate_at = 0;
string text = NormalizeTargetCandidateId(candidate_id);
if(StringLen(text) != 12)
return false;
int year = (int)StringToInteger(StringSubstr(text, 0, 4));
int month = (int)StringToInteger(StringSubstr(text, 4, 2));
int day = (int)StringToInteger(StringSubstr(text, 6, 2));
int hour = (int)StringToInteger(StringSubstr(text, 8, 2));
int minute = (int)StringToInteger(StringSubstr(text, 10, 2));
if(year < 2000 || month < 1 || month > 12 ||
day < 1 || day > 31 || hour < 0 || hour > 23 ||
minute < 0 || minute > 59)
return false;
string datetime_text = StringFormat("%04d.%02d.%02d %02d:%02d",
year, month, day, hour, minute);
datetime parsed = StringToTime(datetime_text);
if(parsed <= 0)
return false;
MqlDateTime parts;
TimeToStruct(parsed, parts);
if(parts.year != year || parts.mon != month || parts.day != day ||
parts.hour != hour || parts.min != minute)
return false;
candidate_at = parsed;
return true;
}
//+------------------------------------------------------------------+
//| バッチファイル(Pythonスクリプト)を実行する関数
//+------------------------------------------------------------------+
/**
* @brief H4トレンド判定用バッチファイルを起動します。
*
* @return 起動に成功した場合はtrue、Python実行中または起動失敗時はfalse。
*
* 起動直前に`process_done_trend.txt`を削除し、プロセスハンドルを保持して終了確認できる状態にします。
*/
bool ExecuteBatchTrend()
{
if(!IsProcessStartAllowed(done_trend_file, running_trend_file, "trend", g_trend_process))
return false;
DeleteDoneFile(done_trend_file);
int price_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
return StartBatchProcess(get_trend_reply_bat, running_trend_file, "trend",
g_trend_process, mt5_file_prefix, price_digits);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief H1エントリー価格生成用バッチファイルを起動します。
*
* @return 起動に成功した場合はtrue、Python実行中または起動失敗時はfalse。
*
* 起動直前に`process_done_entry.txt`を削除し、プロセスハンドルを保持して終了確認できる状態にします。
*/
bool ExecuteBatchEntry()
{
if(!IsProcessStartAllowed(done_entry_file, running_entry_file, "entry", g_entry_process))
return false;
DeleteDoneFile(done_entry_file);
int price_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
return StartBatchProcess(get_entry_reply_bat, running_entry_file, "entry",
g_entry_process, mt5_file_prefix, price_digits);
}
//+------------------------------------------------------------------+
//| "ohlc.csv"を出力後、バッチファイルの実行する関数
//+------------------------------------------------------------------+
/**
* @brief H4 OHLCをCSV出力し、トレンド判定Pythonを起動します。
*
* @param state EA全体の状態。トレンド読込待ちフラグを更新します。
* @return CSV出力とバッチ起動が開始できた場合はtrue。
*/
bool RecordOHLCAndExecuteBatch_Trend(EAState &state)
{
if(!IsProcessStartAllowed(done_trend_file, running_trend_file, "trend", g_trend_process))
{
Print("Trend Python is still running. Skip H4 CSV update.");
return false;
}
datetime times[];
double open_prices[], high_prices[], low_prices[], close_prices[];
if(!GetLatestOHLC(times, open_prices, high_prices, low_prices, close_prices,
PERIOD_H4, HISTORY_BARS))
{ Print("GetLatestOHLC(H4) failed."); return false; }
if(!RecordOHLC(ohlc_h4_file, times, open_prices, high_prices, low_prices, close_prices))
{ Print("RecordOHLC(H4) failed."); return false; }
if(!ExecuteBatchTrend())
return false;
state.load_trend_flg = true;
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief H1 OHLCをCSV出力し、エントリー価格生成Pythonを起動します。
*
* @param state EA全体の状態。ターゲット価格読込待ちフラグを更新します。
* @return CSV出力とバッチ起動が開始できた場合はtrue。
*/
bool RecordOHLCAndExecuteBatch_Entry(EAState &state)
{
if(!IsProcessStartAllowed(done_entry_file, running_entry_file, "entry", g_entry_process))
{
Print("Entry Python is still running. Skip H1 CSV update.");
return false;
}
datetime times[];
double open_prices[], high_prices[], low_prices[], close_prices[];
if(!GetLatestOHLC(times, open_prices, high_prices, low_prices, close_prices,
PERIOD_H1, HISTORY_BARS))
{ Print("GetLatestOHLC(H1) failed."); return false; }
if(!RecordOHLC(ohlc_h1_file, times, open_prices, high_prices, low_prices, close_prices))
{ Print("RecordOHLC(H1) failed."); return false; }
if(!ExecuteBatchEntry())
return false;
state.load_target_flg = true;
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief Pythonが出力したトレンド判定結果を必要なタイミングで読み込みます。
*
* @param state EA全体の状態。H4 market_stateと更新フラグを更新します。
* @return 現状は常にtrue。
*/
bool GetTrendState(EAState &state)
{
bool ProcessDone = CheckDoneFile(done_trend_file);
if(ProcessDone && g_ea.load_trend_flg)
{
int trend_state;
LoadTrendState(trend_state);
state.trend_state = trend_state;
Print("market_state: ", state.trend_state, " (", MarketStateName(state.trend_state), ")");
g_ea.load_trend_flg = false;
g_ea.last_trend_update = TimeLocal();
}
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
/**
* @brief `trend_state.txt` からH4 market_state判定値を読み込みます。
*
* @param trend_state 読み込んだ値を格納する参照。0..5のmarket_state。6は技術エラー停止。
*
* ファイル未存在、読込失敗、異常値の場合は安全側として6(技術エラー停止)を設定します。
*/
void LoadTrendState(int &trend_state)
{
string filename = trend_state_file;
trend_state = MARKET_TECHNICAL_ERROR_STOP;
if(FileIsExist(filename))
{
int filehandle = FileOpen(filename, FILE_READ | FILE_TXT);
if(filehandle != INVALID_HANDLE)
{
string line = FileReadString(filehandle);
int value = MARKET_TECHNICAL_ERROR_STOP;
if(TryParseStrictInteger(line, value) &&
value >= MARKET_LOW_VOL_RANGE && value <= MARKET_TECHNICAL_ERROR_STOP)
trend_state = value;
else
Print("Invalid market_state value: ", line, ". Use technical error stop(6).");
FileClose(filehandle);
}
else
{ Print("Failed to open trend_state.txt"); trend_state = MARKET_TECHNICAL_ERROR_STOP; }
}
else
{ Print("trend_state.txt not found"); trend_state = MARKET_TECHNICAL_ERROR_STOP; }
}
//+------------------------------------------------------------------+
//| ターゲット価格を取得する関数
//+------------------------------------------------------------------+
/**
* @brief Pythonが出力したエントリー価格群を必要なタイミングで読み込みます。
*
* @param state EA全体の状態。`res_chk` と4タイプ分のen/tp/slを更新します。
* @return 現状は常にtrue。
*/
bool GetTargetPrices(EAState &state)
{
bool ProcessDone = CheckDoneFile(done_entry_file); // ←変更
if(ProcessDone && g_ea.load_target_flg) // ←変更
{
double target_prices[];
LoadTargetPrices(target_prices);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
// 先頭は res_chk
state.res_chk = (int)target_prices[0];
// ★ 1..4 の (en,tp,sl) を読む:合計12個
for(int t=1; t<=4; t++)
{
int base = 1 + (t-1)*3; // 1,4,7,10
state.en_price[t] = NormalizeDouble(target_prices[base + 0], digits);
state.tp_price[t] = NormalizeDouble(target_prices[base + 1], digits);
state.sl_price[t] = NormalizeDouble(target_prices[base + 2], digits);
}
// ログ(任意)
Print("target_prices: res=", state.res_chk,
" | T1 en=", state.en_price[1], " tp=", state.tp_price[1], " sl=", state.sl_price[1],
" | T2 en=", state.en_price[2], " tp=", state.tp_price[2], " sl=", state.sl_price[2],
" | T3 en=", state.en_price[3], " tp=", state.tp_price[3], " sl=", state.sl_price[3],
" | T4 en=", state.en_price[4], " tp=", state.tp_price[4], " sl=", state.sl_price[4]);
LoadTargetZones(state);
if(use_split_entry_zone && cancel_old_split_pending_on_new_zone)
{
if(state.zone_res_chk == 1)
CancelStaleSplitPendingOrders(state.zone_candidate_id);
else
CancelStaleSplitPendingOrders("");
}
g_ea.load_target_flg = false; // ←変更
g_ea.last_target_update = TimeLocal(); // ←変更
g_ea.target_loaded_at = TimeCurrent();
}
return true;
}
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
//| "target_prices.txt"を読み込む関数
//+------------------------------------------------------------------+
/**
* @brief `target_prices.txt` から13個の数値を読み込みます。
*
* @param target_prices 読み込んだ数値を格納する配列。先頭がres_chk、以降は4タイプ分のen/tp/sl。
*
* 行数不足の場合は`res_chk=0`として、ターゲット価格を無効扱いにします。
*/
void LoadTargetPrices(double &target_prices[])
{
ArrayResize(target_prices, TARGET_SIZE);
for(int i = 0; i < TARGET_SIZE; i++)
target_prices[i] = DEFAULT_TARGET_PRICE;
string filename = target_prices_file;
if(FileIsExist(filename))
{
int filehandle = FileOpen(filename, FILE_READ | FILE_TXT);
if(filehandle != INVALID_HANDLE)
{
int i = 0;
bool parse_failed = false;
while(!FileIsEnding(filehandle) && i < TARGET_SIZE)
{
string line = FileReadString(filehandle);
double parsed = 0.0;
if(!TryParseStrictDouble(line, parsed))
{
Print("target_prices.txt invalid numeric line. index=", i, " line=", line);
parse_failed = true;
break;
}
target_prices[i] = parsed;
i++;
}
FileClose(filehandle);
if(parse_failed || i < TARGET_SIZE)
{
if(!parse_failed)
Print("target_prices.txt line count is short. loaded=", i, " required=", TARGET_SIZE);
for(int j = 0; j < TARGET_SIZE; j++)
target_prices[j] = DEFAULT_TARGET_PRICE;
target_prices[0] = 0; // 不完全なファイルは無効扱い
}
}
else
{
Print("Failed to open file. Error code: ", GetLastError());
}
}
else
{
Print("File does not exist: ", filename);
}
}
//+------------------------------------------------------------------+
//| target_zones.txt の状態を停止値へ初期化する関数
//+------------------------------------------------------------------+
void ResetTargetZones(EAState &state)
{
state.zone_res_chk = 0;
state.zone_candidate_id = "0";
state.target_candidate_at = 0;
for(int t = 1; t <= 4; t++)
{
state.zone_low[t] = 0.0;
state.zone_high[t] = 0.0;
state.zone_tp[t] = 0.0;
state.zone_sl[t] = 0.0;
}
}
//+------------------------------------------------------------------+
//| target_zones.txt の1戦略行を読み込む関数
//+------------------------------------------------------------------+
bool ParseTargetZoneLine(const string line, EAState &state, const int digits)
{
string parts[];
int count = StringSplit(line, StringGetCharacter(",", 0), parts);
if(count < 5)
return false;
int strategy = 0;
if(!TryParseStrictInteger(parts[0], strategy))
return false;
if(strategy < 1 || strategy > 4)
return false;
double zone_low = 0.0;
double zone_high = 0.0;
double zone_tp = 0.0;
double zone_sl = 0.0;
if(!TryParseStrictDouble(parts[1], zone_low) ||
!TryParseStrictDouble(parts[2], zone_high) ||
!TryParseStrictDouble(parts[3], zone_tp) ||
!TryParseStrictDouble(parts[4], zone_sl))
return false;
state.zone_low[strategy] = NormalizeDouble(zone_low, digits);
state.zone_high[strategy] = NormalizeDouble(zone_high, digits);
state.zone_tp[strategy] = NormalizeDouble(zone_tp, digits);
state.zone_sl[strategy] = NormalizeDouble(zone_sl, digits);
return true;
}
//+------------------------------------------------------------------+
//| target_zones.txt を読み込む関数
//+------------------------------------------------------------------+
void LoadTargetZones(EAState &state)
{
ResetTargetZones(state);
string filename = target_zones_file;
if(!FileIsExist(filename))
{
Print("target_zones.txt not found. Split entry zones disabled for this candidate.");
return;
}
int filehandle = FileOpen(filename, FILE_READ | FILE_TXT);
if(filehandle == INVALID_HANDLE)
{
Print("Failed to open target_zones.txt. Error code: ", GetLastError());
return;
}
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
int line_index = 0;
int loaded_strategies = 0;
while(!FileIsEnding(filehandle))
{
string line = FileReadString(filehandle);
if(line == "")
continue;
line_index++;
if(line_index == 1)
{
int schema_version = 0;
if(!TryParseStrictInteger(line, schema_version) ||
schema_version != TARGET_ZONE_SCHEMA_VERSION)
{
Print("target_zones schema mismatch. loaded=", line,
" required=", TARGET_ZONE_SCHEMA_VERSION);
FileClose(filehandle);
ResetTargetZones(state);
return;
}
continue;
}
if(line_index == 2)
{
int zone_res = 0;
if(!TryParseStrictInteger(line, zone_res))
{
Print("target_zones invalid res_chk line: ", line);
FileClose(filehandle);
ResetTargetZones(state);
return;
}
state.zone_res_chk = zone_res;
continue;
}
if(line_index == 3)
{
state.zone_candidate_id = NormalizeTargetCandidateId(line);
datetime candidate_at = 0;
if(TryParseTargetCandidateTime(state.zone_candidate_id, candidate_at))
state.target_candidate_at = candidate_at;
else
state.target_candidate_at = 0;
continue;
}
if(ParseTargetZoneLine(line, state, digits))
loaded_strategies++;
}
FileClose(filehandle);
if(line_index < 7 || loaded_strategies < 4)
{
Print("target_zones.txt line count is short. loaded_lines=", line_index,
" loaded_strategies=", loaded_strategies);
ResetTargetZones(state);
return;
}
if(state.zone_res_chk != 1)
ResetTargetZones(state);
Print("target_zones: res=", state.zone_res_chk,
" id=", state.zone_candidate_id,
" candidate_at=", TimeToString(state.target_candidate_at, TIME_DATE | TIME_MINUTES),
" | T1 zone=", state.zone_low[1], "-", state.zone_high[1],
" tp=", state.zone_tp[1], " sl=", state.zone_sl[1],
" | T2 zone=", state.zone_low[2], "-", state.zone_high[2],
" tp=", state.zone_tp[2], " sl=", state.zone_sl[2],
" | T3 zone=", state.zone_low[3], "-", state.zone_high[3],
" tp=", state.zone_tp[3], " sl=", state.zone_sl[3],
" | T4 zone=", state.zone_low[4], "-", state.zone_high[4],
" tp=", state.zone_tp[4], " sl=", state.zone_sl[4]);
}
//+------------------------------------------------------------------+
#endif
+250 -56
View File
@@ -73,10 +73,12 @@ int VolumeDigits()
* @param tp 利確価格。
* @param sl 損切価格。
* @param volume 注文ロット。
* @param comment_suffix 分割注文識別用のコメント接尾辞。空なら従来コメント。
* @param comment_suffix H1候補または分割注文識別用のコメント接尾辞。空なら従来コメント。
* @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。
* @return 注文が正常に受理された場合はtrue、それ以外はfalse。
*/
bool SendOrder(int orderType, double price, double tp, double sl, double volume, string comment_suffix)
bool SendOrder(int orderType, double price, double tp, double sl, double volume,
string comment_suffix, datetime candidate_reference_time)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
@@ -95,7 +97,8 @@ bool SendOrder(int orderType, double price, double tp, double sl, double volume,
// 指値または逆指値注文
request.action = TRADE_ACTION_PENDING;
ApplyPendingOrderExpiration(request);
if(!ApplyPendingOrderExpiration(request, candidate_reference_time))
return false;
switch(orderType)
{
@@ -178,35 +181,48 @@ ENUM_ORDER_TYPE_FILLING GetOrderFillingPolicy(string symbol)
* @brief brokerが対応している範囲でpending orderにサーバー側の期限を設定します。
*
* @param request 更新対象の取引リクエスト。
* @param candidate_reference_time H1候補の元になった確定足時刻。取得不能時は0。
* @return H1候補の期限が残っている場合はtrue。発注前に期限切れならfalse。
*/
void ApplyPendingOrderExpiration(MqlTradeRequest &request)
bool ApplyPendingOrderExpiration(MqlTradeRequest &request, const datetime candidate_reference_time)
{
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE);
datetime expiration_at = TimeCurrent() + expiration_seconds;
if(candidate_reference_time > 0)
expiration_at = candidate_reference_time + expiration_seconds;
if(expiration_seconds > 0 &&
(expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0)
if(expiration_seconds <= 0 || expiration_at <= TimeCurrent())
{
request.type_time = ORDER_TIME_SPECIFIED;
request.expiration = TimeCurrent() + expiration_seconds;
return;
Print("[Order Skip] H1 candidate expiration is not in the future. candidate_at=",
TimeToString(candidate_reference_time, TIME_DATE | TIME_SECONDS),
" expiration_at=", TimeToString(expiration_at, TIME_DATE | TIME_SECONDS));
return false;
}
if(expiration_seconds > 0 &&
(expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0)
long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE);
if((expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0)
{
request.type_time = ORDER_TIME_SPECIFIED;
request.expiration = expiration_at;
return true;
}
if((expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0)
{
request.type_time = ORDER_TIME_SPECIFIED_DAY;
request.expiration = TimeCurrent() + expiration_seconds;
return;
request.expiration = expiration_at;
return true;
}
if((expiration_mode & SYMBOL_EXPIRATION_DAY) != 0)
{
request.type_time = ORDER_TIME_DAY;
return;
return true;
}
request.type_time = ORDER_TIME_GTC;
return true;
}
//+------------------------------------------------------------------+
@@ -391,6 +407,215 @@ bool IsCurrentSplitCandidateComment(const string comment, const string candidate
return (StringFind(comment, "Z" + candidate_id + "#") >= 0);
}
//+------------------------------------------------------------------+
//| pending取消のticket単位クールダウン管理
//+------------------------------------------------------------------+
// Prevent repeated server-side cancel requests for the same pending ticket.
int EffectiveCancelRetryCooldownSeconds()
{
int cooldown = input_cancel_retry_cooldown_seconds;
if(cooldown < 1)
cooldown = 1;
if(cooldown > 3600)
cooldown = 3600;
return cooldown;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int FindPendingOrderCancelAttempt(const ulong ticket)
{
for(int i = ArraySize(g_cancel_retry_tickets) - 1; i >= 0; i--)
{
if(g_cancel_retry_tickets[i] == ticket)
return i;
}
return -1;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void RemovePendingOrderCancelAttemptAt(const int index)
{
int size = ArraySize(g_cancel_retry_tickets);
if(index < 0 || index >= size)
return;
int last = size - 1;
if(index != last)
{
g_cancel_retry_tickets[index] = g_cancel_retry_tickets[last];
g_cancel_retry_at[index] = g_cancel_retry_at[last];
}
ArrayResize(g_cancel_retry_tickets, last);
ArrayResize(g_cancel_retry_at, last);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void ForgetPendingOrderCancelAttempt(const ulong ticket)
{
RemovePendingOrderCancelAttemptAt(FindPendingOrderCancelAttempt(ticket));
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CanAttemptPendingOrderCancel(const ulong ticket)
{
int index = FindPendingOrderCancelAttempt(ticket);
if(index < 0)
return true;
return (TimeCurrent() - g_cancel_retry_at[index] >= EffectiveCancelRetryCooldownSeconds());
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void MarkPendingOrderCancelAttempt(const ulong ticket)
{
int index = FindPendingOrderCancelAttempt(ticket);
if(index >= 0)
{
g_cancel_retry_at[index] = TimeCurrent();
return;
}
int size = ArraySize(g_cancel_retry_tickets);
ArrayResize(g_cancel_retry_tickets, size + 1);
ArrayResize(g_cancel_retry_at, size + 1);
g_cancel_retry_tickets[size] = ticket;
g_cancel_retry_at[size] = TimeCurrent();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void CleanupPendingOrderCancelAttempts()
{
for(int i = ArraySize(g_cancel_retry_tickets) - 1; i >= 0; i--)
{
if(!OrderSelect(g_cancel_retry_tickets[i]))
RemovePendingOrderCancelAttemptAt(i);
}
}
//+------------------------------------------------------------------+
//| pending取消要求を共通化する関数
//+------------------------------------------------------------------+
/**
* @brief 同一ticketへの取消要求をクールダウンし、取引サーバーへの過剰送信を防ぎます。
*/
bool TryCancelPendingOrder(const ulong ticket, const string reason)
{
if(ticket == 0 || !CanAttemptPendingOrderCancel(ticket))
return false;
MarkPendingOrderCancelAttempt(ticket);
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
request.action = TRADE_ACTION_REMOVE;
request.order = ticket;
ResetLastError();
if(!OrderSend(request, trade_result))
{
Print("Failed to delete order. Ticket: ", ticket,
" Reason: ", reason, " Error: ", GetLastError(),
" RetryAfterSeconds: ", EffectiveCancelRetryCooldownSeconds());
return false;
}
if(trade_result.retcode == TRADE_RETCODE_DONE)
{
Print("Pending order canceled. Ticket: ", ticket, " Reason: ", reason);
ForgetPendingOrderCancelAttempt(ticket);
return true;
}
Print("Order cancellation failed. Ticket: ", ticket,
" Reason: ", reason, " Retcode: ", trade_result.retcode,
" RetryAfterSeconds: ", EffectiveCancelRetryCooldownSeconds());
return false;
}
//+------------------------------------------------------------------+
//| pending注文タイプをEA戦略番号へ変換する関数
//+------------------------------------------------------------------+
int PendingOrderStrategy(const ENUM_ORDER_TYPE order_type)
{
switch(order_type)
{
case ORDER_TYPE_BUY_STOP:
return 1;
case ORDER_TYPE_BUY_LIMIT:
return 2;
case ORDER_TYPE_SELL_STOP:
return 3;
case ORDER_TYPE_SELL_LIMIT:
return 4;
default:
return 0;
}
}
//+------------------------------------------------------------------+
//| H4 market_stateと矛盾するpending注文を取消する関数
//+------------------------------------------------------------------+
// Cancel pending orders that contradict the refreshed H4 market state.
bool CancelPendingOrdersNotAllowedByTrend(const int trend_state)
{
bool result = false;
CleanupPendingOrderCancelAttempts();
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0 || !OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
int strategy = PendingOrderStrategy((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE));
if(strategy > 0 && IsOrderTypeAllowedByTrend(strategy, trend_state))
continue;
string reason = "H4 market_state=" + IntegerToString(trend_state) +
" strategy=" + IntegerToString(strategy);
if(TryCancelPendingOrder(ticket, reason))
result = true;
}
return result;
}
//+------------------------------------------------------------------+
//| pendingコメントからH1候補時刻を復元する関数
//+------------------------------------------------------------------+
datetime PendingOrderCandidateTime(const string comment)
{
int marker = StringFind(comment, "Z");
if(marker < 0)
return 0;
datetime candidate_at = 0;
string candidate_id = StringSubstr(comment, marker + 1, 12);
if(!TryParseTargetCandidateTime(candidate_id, candidate_at))
return 0;
return candidate_at;
}
//+------------------------------------------------------------------+
//| 分割slotが既存注文/ポジションに存在するか判定する関数
//+------------------------------------------------------------------+
@@ -437,6 +662,7 @@ bool CancelStaleSplitPendingOrders(const string current_candidate_id)
{
bool result = false;
bool keep_current_candidate = (current_candidate_id != "" && current_candidate_id != "0");
CleanupPendingOrderCancelAttempts();
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
@@ -456,26 +682,8 @@ bool CancelStaleSplitPendingOrders(const string current_candidate_id)
if(keep_current_candidate && IsCurrentSplitCandidateComment(comment, current_candidate_id))
continue;
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
request.action = TRADE_ACTION_REMOVE;
request.order = ticket;
if(!OrderSend(request, trade_result))
{
Print("Failed to delete stale split order. Ticket: ", ticket, " Error: ", GetLastError());
continue;
}
if(trade_result.retcode == TRADE_RETCODE_DONE)
{
Print("Stale split order canceled. Ticket: ", ticket, " comment=", comment);
if(TryCancelPendingOrder(ticket, "stale split comment=" + comment))
result = true;
}
else
{
Print("Stale split order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode);
}
}
return result;
@@ -495,6 +703,7 @@ bool CancelStaleSplitPendingOrders(const string current_candidate_id)
bool CancelExpiredOrders()
{
bool result = false;
CleanupPendingOrderCancelAttempts();
// 未決済注文の数を取得
for(int i = OrdersTotal() - 1; i >= 0; i--)
@@ -508,33 +717,18 @@ bool CancelExpiredOrders()
continue;
datetime open_time = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
datetime reference_time = PendingOrderCandidateTime(OrderGetString(ORDER_COMMENT));
if(reference_time <= 0)
reference_time = open_time;
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
if(open_time <= 0 || expiration_seconds <= 0)
if(reference_time <= 0 || expiration_seconds <= 0)
continue;
if(TimeCurrent() - open_time >= expiration_seconds)
if(TimeCurrent() - reference_time >= expiration_seconds)
{
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
request.action=TRADE_ACTION_REMOVE; // 取引操作タイプ
request.order = ticket; // 注文チケット
if(!OrderSend(request, trade_result)) // 削除リクエスト送信
{
Print("Failed to delete order. Ticket: ", ticket, " Error: ", GetLastError());
}
else
{
if(trade_result.retcode == TRADE_RETCODE_DONE)
{
Print("Order canceled successfully due to a time limit. Ticket: ", ticket);
result = true; // 削除成功フラグ
}
else
{
Print("Order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode);
}
}
if(TryCancelPendingOrder(ticket, "H1 candidate expired"))
result = true;
}
}
}
@@ -0,0 +1,625 @@
#ifndef HIT_TRADE_MANAGER_MQH
#define HIT_TRADE_MANAGER_MQH
//+------------------------------------------------------------------+
//| 注文ロットがブローカー制約を満たすか判定する関数
//+------------------------------------------------------------------+
/**
* @brief 通常注文と分割注文の最終送信前に volume min/max/step を検証します。
*/
bool IsOrderVolumeAllowed(const double volume, const string context)
{
double min_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double max_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double step_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(volume <= 0.0 || min_volume <= 0.0 || max_volume <= 0.0 || step_volume <= 0.0)
{
Print("[Order Skip] invalid volume setting. context=", context,
" volume=", volume, " min=", min_volume,
" max=", max_volume, " step=", step_volume);
return false;
}
if(volume < min_volume || volume > max_volume)
{
Print("[Order Skip] volume out of range. context=", context,
" volume=", volume, " min=", min_volume, " max=", max_volume);
return false;
}
double steps = (volume - min_volume) / step_volume;
double nearest = MathRound(steps);
if(MathAbs(steps - nearest) > 0.000001)
{
Print("[Order Skip] volume does not match broker step. context=", context,
" volume=", volume, " min=", min_volume, " step=", step_volume);
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| ブローカーのロットstepから正規化桁数を推定する関数
//+------------------------------------------------------------------+
int VolumeDigits()
{
int volume_digits = 2;
double step_volume = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(step_volume > 0.0)
{
volume_digits = 0;
double step = step_volume;
while(step < 1.0 && volume_digits < 8)
{
step *= 10.0;
volume_digits++;
}
}
return volume_digits;
}
//+------------------------------------------------------------------+
//| エントリー注文を送信する関数 (1:buy-stop, 2:buy-limit, 3:sell-stop, 4:sell-limit)
//+------------------------------------------------------------------+
/**
* @brief 指定された注文タイプでペンディング注文を送信します。
*
* @param orderType 注文タイプ。1=Buy Stop、2=Buy Limit、3=Sell Stop、4=Sell Limit。
* @param price エントリー価格。
* @param tp 利確価格。
* @param sl 損切価格。
* @param volume 注文ロット。
* @param comment_suffix 分割注文識別用のコメント接尾辞。空なら従来コメント。
* @return 注文が正常に受理された場合はtrue、それ以外はfalse。
*/
bool SendOrder(int orderType, double price, double tp, double sl, double volume, string comment_suffix)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.magic = magic_number;
request.symbol = _Symbol;
request.volume = NormalizeDouble(volume, VolumeDigits());
request.deviation = slippage;
request.type_filling = GetOrderFillingPolicy(_Symbol);
request.price = NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.tp = NormalizeDouble(tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.sl = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
request.comment = RequestComment(orderType, comment_suffix);
string orderTypeStr = "";
// 指値または逆指値注文
request.action = TRADE_ACTION_PENDING;
ApplyPendingOrderExpiration(request);
switch(orderType)
{
case 1: // 順張り買い (buy-stop)
request.type = ORDER_TYPE_BUY_STOP;
orderTypeStr = "buy-stop";
break;
case 2: // 逆張り買い (buy-limit)
request.type = ORDER_TYPE_BUY_LIMIT;
orderTypeStr = "buy-limit";
break;
case 3: // 順張り売り (sell-stop)
request.type = ORDER_TYPE_SELL_STOP;
orderTypeStr = "sell-stop";
break;
case 4: // 逆張り売り (sell-limit)
request.type = ORDER_TYPE_SELL_LIMIT;
orderTypeStr = "sell-limit";
break;
default: // 不正なorderType
Print(__FUNCTION__, ": invalid orderType=", orderType);
return false;
}
string volume_context = orderTypeStr;
if(comment_suffix != "")
volume_context += " " + comment_suffix;
if(!IsOrderVolumeAllowed(volume, volume_context))
return false;
// 注文送信
if(!OrderSend(request, result))
{
Print(__FUNCTION__, ": OrderSend failed. err=", GetLastError());
return false;
}
// 注文成功確認
if(result.retcode == TRADE_RETCODE_PLACED ||
result.retcode == TRADE_RETCODE_DONE ||
result.retcode == TRADE_RETCODE_DONE_PARTIAL)
{
Print(request.comment + " - " + orderTypeStr + " order success (ticket=", result.order, ")");
return true;
}
else
{
Print(__FUNCTION__, ": OrderSend retcode=", result.retcode);
return false;
}
}
//+------------------------------------------------------------------+
//| 注文の執行ポリシーを取得する関数
//+------------------------------------------------------------------+
/**
* @brief シンボルに対応する注文執行ポリシーを取得します。
*
* @param symbol 対象シンボル。
* @return 利用可能な執行ポリシー。IOC、FOK、RETURNの順で選択します。
*/
ENUM_ORDER_TYPE_FILLING GetOrderFillingPolicy(string symbol)
{
long fill_mode = SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
if((fill_mode & SYMBOL_FILLING_IOC) != 0)
return ORDER_FILLING_IOC;
if((fill_mode & SYMBOL_FILLING_FOK) != 0)
return ORDER_FILLING_FOK;
return ORDER_FILLING_RETURN;
}
//+------------------------------------------------------------------+
//| ペンディング注文の有効期限を設定する関数
//+------------------------------------------------------------------+
/**
* @brief brokerが対応している範囲でpending orderにサーバー側の期限を設定します。
*
* @param request 更新対象の取引リクエスト。
*/
void ApplyPendingOrderExpiration(MqlTradeRequest &request)
{
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
long expiration_mode = SymbolInfoInteger(request.symbol, SYMBOL_EXPIRATION_MODE);
if(expiration_seconds > 0 &&
(expiration_mode & SYMBOL_EXPIRATION_SPECIFIED) != 0)
{
request.type_time = ORDER_TIME_SPECIFIED;
request.expiration = TimeCurrent() + expiration_seconds;
return;
}
if(expiration_seconds > 0 &&
(expiration_mode & SYMBOL_EXPIRATION_SPECIFIED_DAY) != 0)
{
request.type_time = ORDER_TIME_SPECIFIED_DAY;
request.expiration = TimeCurrent() + expiration_seconds;
return;
}
if((expiration_mode & SYMBOL_EXPIRATION_DAY) != 0)
{
request.type_time = ORDER_TIME_DAY;
return;
}
request.type_time = ORDER_TIME_GTC;
}
//+------------------------------------------------------------------+
//| 注文コメント(ローカル時刻)を生成する関数(先頭に orderType を付与)
//+------------------------------------------------------------------+
/**
* @brief 注文コメント文字列を生成します。
*
* @param orderType 注文タイプ番号。
* @param comment_suffix 分割注文識別用のコメント接尾辞。
* @return 注文タイプとPCローカル時刻、または分割注文識別子を含むコメント文字列。
*/
string RequestComment(int orderType, string comment_suffix)
{
if(comment_suffix != "")
return "T" + IntegerToString(orderType) + " " + comment_suffix;
datetime localTime = TimeLocal();
string timeStr = TimeToString(localTime, TIME_DATE|TIME_MINUTES);
// 先頭に 1,2,3,4 を付ける(例: "1 | PC Time: 2026.02.14 19:05"
return IntegerToString(orderType) + " | PC Time: " + timeStr;
}
//+------------------------------------------------------------------+
//| H4 market_stateに対して許可された注文タイプか判定する関数
//+------------------------------------------------------------------+
/**
* @brief H4 market_stateに対して指定注文タイプが許可されるか判定します。
*
* @param orderType 注文タイプ番号。
* @param trend_state H4 market_state0..5、6は技術エラー停止)。
* @return 許可される注文タイプならtrue。技術エラー停止または異常値ではfalse。
*/
bool IsOrderTypeAllowedByTrend(const int orderType, const int trend_state)
{
switch(trend_state)
{
case MARKET_LOW_VOL_RANGE:
case MARKET_HIGH_VOL_RANGE:
return (orderType == 2 || orderType == 4); // Range: Buy Limit / Sell Limitのみ
case MARKET_LOW_VOL_UP:
case MARKET_HIGH_VOL_UP:
return (orderType == 1 || orderType == 2); // Up: Buy Stop / Buy Limitのみ
case MARKET_LOW_VOL_DOWN:
case MARKET_HIGH_VOL_DOWN:
return (orderType == 3 || orderType == 4); // Down: Sell Stop / Sell Limitのみ
case MARKET_TECHNICAL_ERROR_STOP:
default:
return false; // 技術エラー停止または異常値は新規注文しない
}
}
//+------------------------------------------------------------------+
//| target_prices.txt の価格が有効か判定する関数
//+------------------------------------------------------------------+
/**
* @brief エントリー価格、利確価格、損切価格が有効値か判定します。
*
* @param en エントリー価格。
* @param tp 利確価格。
* @param sl 損切価格。
* @return すべて0より大きい場合はtrue。
*/
bool HasValidTargetPrices(const double en, const double tp, const double sl)
{
return (en > 0.0 && tp > 0.0 && sl > 0.0);
}
//+------------------------------------------------------------------+
//| 自EAの未約定注文数を数える関数
//+------------------------------------------------------------------+
/**
* @brief このEAが管理対象とする未約定注文数を数えます。
*
* @return `_Symbol` と `magic_number` が一致する未約定注文数。
*/
int CountMyPendingOrders()
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0)
continue;
if(!OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
count++;
}
return count;
}
//+------------------------------------------------------------------+
//| 自EAのポジション数を数える関数
//+------------------------------------------------------------------+
/**
* @brief このEAが管理対象とする保有ポジション数を数えます。
*
* @return `_Symbol` と `magic_number` が一致するポジション数。
*/
int CountMyPositions()
{
int count = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string symbol = PositionGetSymbol(i);
if(symbol == "")
continue;
if(symbol != _Symbol)
continue;
if((int)PositionGetInteger(POSITION_MAGIC) != magic_number)
continue;
count++;
}
return count;
}
//+------------------------------------------------------------------+
//| 自EAの注文+ポジション数を数える関数
//+------------------------------------------------------------------+
/**
* @brief このEAが使用中の注文数とポジション数の合計を返します。
*
* @return 未約定注文数 + 保有ポジション数。
*/
int CountMyUsed()
{
return CountMyPendingOrders() + CountMyPositions();
}
//+------------------------------------------------------------------+
//| 実効ポジション上限を返す関数
//+------------------------------------------------------------------+
/**
* @brief input指定の上限を安全な範囲に丸めて返します。
*/
int EffectivePositionLimit()
{
int limit = input_position_limit;
if(limit < 1)
limit = 1;
if(limit > POSITION_LIMIT)
limit = POSITION_LIMIT;
return limit;
}
//+------------------------------------------------------------------+
//| 分割注文コメントか判定する関数
//+------------------------------------------------------------------+
bool IsSplitOrderComment(const string comment)
{
return (StringFind(comment, " Z") >= 0 && StringFind(comment, "#") >= 0);
}
//+------------------------------------------------------------------+
//| 指定候補IDの分割注文コメントか判定する関数
//+------------------------------------------------------------------+
bool IsCurrentSplitCandidateComment(const string comment, const string candidate_id)
{
if(candidate_id == "" || candidate_id == "0")
return false;
return (StringFind(comment, "Z" + candidate_id + "#") >= 0);
}
//+------------------------------------------------------------------+
//| 分割slotが既存注文/ポジションに存在するか判定する関数
//+------------------------------------------------------------------+
bool HasExistingSplitSlot(const string slot_key)
{
if(slot_key == "")
return false;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0)
continue;
if(!OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
if(StringFind(OrderGetString(ORDER_COMMENT), slot_key) >= 0)
return true;
}
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string symbol = PositionGetSymbol(i);
if(symbol == "")
continue;
if(symbol != _Symbol)
continue;
if((int)PositionGetInteger(POSITION_MAGIC) != magic_number)
continue;
if(StringFind(PositionGetString(POSITION_COMMENT), slot_key) >= 0)
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| 古い分割pending注文を取消する関数
//+------------------------------------------------------------------+
bool CancelStaleSplitPendingOrders(const string current_candidate_id)
{
bool result = false;
bool keep_current_candidate = (current_candidate_id != "" && current_candidate_id != "0");
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(ticket == 0)
continue;
if(!OrderSelect(ticket))
continue;
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if((int)OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
string comment = OrderGetString(ORDER_COMMENT);
if(!IsSplitOrderComment(comment))
continue;
if(keep_current_candidate && IsCurrentSplitCandidateComment(comment, current_candidate_id))
continue;
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
request.action = TRADE_ACTION_REMOVE;
request.order = ticket;
if(!OrderSend(request, trade_result))
{
Print("Failed to delete stale split order. Ticket: ", ticket, " Error: ", GetLastError());
continue;
}
if(trade_result.retcode == TRADE_RETCODE_DONE)
{
Print("Stale split order canceled. Ticket: ", ticket, " comment=", comment);
result = true;
}
else
{
Print("Stale split order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode);
}
}
return result;
}
//+------------------------------------------------------------------+
//| 時間が経過したエントリー注文をキャンセルする関数
//+------------------------------------------------------------------+
/**
* @brief 実時間でENTRY_H1_LIMIT時間を超えた未約定注文をキャンセルします。
*
* @return 1件以上キャンセルに成功した場合はtrue。
*
* 対象は`_Symbol` と `magic_number` が一致する未約定注文のみです。
*/
bool CancelExpiredOrders()
{
bool result = false;
// 未決済注文の数を取得
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i);
if(OrderSelect(ticket)) // 注文を選択
{
if(OrderGetString(ORDER_SYMBOL) != _Symbol)
continue;
if(OrderGetInteger(ORDER_MAGIC) != magic_number)
continue;
datetime open_time = (datetime)OrderGetInteger(ORDER_TIME_SETUP);
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
if(open_time <= 0 || expiration_seconds <= 0)
continue;
if(TimeCurrent() - open_time >= expiration_seconds)
{
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
request.action=TRADE_ACTION_REMOVE; // 取引操作タイプ
request.order = ticket; // 注文チケット
if(!OrderSend(request, trade_result)) // 削除リクエスト送信
{
Print("Failed to delete order. Ticket: ", ticket, " Error: ", GetLastError());
}
else
{
if(trade_result.retcode == TRADE_RETCODE_DONE)
{
Print("Order canceled successfully due to a time limit. Ticket: ", ticket);
result = true; // 削除成功フラグ
}
else
{
Print("Order cancellation failed. Ticket: ", ticket, " Retcode: ", trade_result.retcode);
}
}
}
}
}
return result;
}
//+------------------------------------------------------------------+
//| 時間が経過したポジションをクローズする関数
//+------------------------------------------------------------------+
/**
* @brief 実時間でCLOSE_H1_LIMIT時間を超えた保有ポジションをクローズします。
*
* @return 1件以上クローズに成功した場合はtrue。
*
* 対象は`_Symbol` と `magic_number` が一致するポジションのみです。
*/
bool CloseExpiredPositions()
{
bool result = false;
// ポジションを逆順でループ
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
string position_symbol = PositionGetSymbol(i);
if(position_symbol!="")
{
// Magic Number とシンボルでフィルタリング
if(position_symbol != _Symbol)
continue;
if(PositionGetInteger(POSITION_MAGIC) == magic_number)
{
// ポジションのエントリー時刻を取得
datetime entry_time = (datetime)PositionGetInteger(POSITION_TIME);
int expiration_seconds = CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1);
if(entry_time <= 0 || expiration_seconds <= 0)
continue;
if(TimeCurrent() - entry_time >= expiration_seconds)
{
MqlTradeRequest request = {};
MqlTradeResult trade_result = {};
// ポジションタイプに応じてリクエストを設定
int position_type = (int)PositionGetInteger(POSITION_TYPE);
request.action = TRADE_ACTION_DEAL;
request.position = PositionGetInteger(POSITION_TICKET); // ポジションのチケット番号
request.symbol = position_symbol; // シンボル
request.volume = PositionGetDouble(POSITION_VOLUME); // ポジションサイズ
request.price = (position_type == POSITION_TYPE_BUY)
? SymbolInfoDouble(position_symbol, SYMBOL_BID) // BUYの場合はBIDでクローズ
: SymbolInfoDouble(position_symbol, SYMBOL_ASK); // SELLの場合はASKでクローズ
request.deviation = slippage;
request.type = (position_type == POSITION_TYPE_BUY)
? ORDER_TYPE_SELL // BUYポジションをSELLでクローズ
: ORDER_TYPE_BUY; // SELLポジションをBUYでクローズ
request.type_filling = GetOrderFillingPolicy(position_symbol); // 注文執行ポリシー
request.magic = magic_number;
// 注文送信
if(!OrderSend(request, trade_result))
{
Print("Failed to close position. Ticket: ", request.position, " Error: ", GetLastError());
continue;
}
// 結果の確認
if(trade_result.retcode == TRADE_RETCODE_DONE || trade_result.retcode == TRADE_RETCODE_DONE_PARTIAL)
{
Print("Position closed successfully due to a time limit. Ticket: ", request.position);
result = true; // 少なくとも1つ成功した場合
}
else
{
Print("Failed to close position. Ticket: ", request.position, " Retcode: ", trade_result.retcode);
}
}
}
}
else
{
Print("Failed to select position at index ", i, ". Error: ", GetLastError());
}
}
return result;
}
#endif
+26 -6
View File
@@ -20,7 +20,7 @@
- `lot_size = 0.01`: Order volume.
- `spread_limit = 60`: Maximum allowed spread in points.
- `magic_number = 10001`: Magic number used to identify this EA's orders and positions.
- `initial_order = 0`: Enables initial H4/H1 processing on startup when set to `1`.
- `initial_order = 0`: Retained for compatibility with older settings. Startup always rebuilds H4/H1 results and remains in technical-error stop state until the H4 result is reloaded.
- `input_position_limit = 10`: Practical limit for this EA's pending orders plus positions. Values above the internal `POSITION_LIMIT` are clamped.
- `use_split_entry_zone = false`: Enables split entries based on H1 predicted zones.
- `split_entry_count = 3`: Number of split entry orders, clamped to 1..10.
@@ -28,6 +28,7 @@
- `split_total_lot_size = 0.09`: Total lot used by total-split mode.
- `split_fixed_lot_size = 0.01`: Per-order lot used by fixed-split mode.
- `cancel_old_split_pending_on_new_zone = true`: Cancels older split pending orders when a new H1 zone set is loaded.
- `input_cancel_retry_cooldown_seconds = 60`: Wait time before retrying a failed pending-order cancellation for the same ticket. The effective value is clamped to 1..3600 seconds.
- `use_m15_entry_filter = true`: Enables M15 closed-bar entry timing confirmation.
- `m15_entry_zone_atr_multiplier = 1.50`: Allowed proximity multiplier based on the M15 average range.
- `use_m15_imbalance_confirmation = true`: Adds M15 impulse confirmation for T1/T3 trend-following candidates.
@@ -148,7 +149,7 @@ New entries are allowed only when all conditions below are satisfied:
- The order-type price relationship and broker minimum distance rules are satisfied.
- When `use_m15_entry_filter = true`, T2/T4 satisfy the M15 direction, rejection, and proximity checks. T1/T3 satisfy M15 impulse confirmation when `use_m15_imbalance_confirmation = true`.
- This EA's pending orders plus positions are below `input_position_limit`.
- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + slot.
- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + order type + slot.
### market_state and Allowed Order Types
@@ -160,6 +161,8 @@ New entries are allowed only when all conditions below are satisfied:
- `5 MARKET_HIGH_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit.
- `6 MARKET_TECHNICAL_ERROR_STOP`: Blocks new entries.
After an H4 `market_state` reload, the EA cancels existing pending orders whose order types are not allowed by the refreshed state. For `6 MARKET_TECHNICAL_ERROR_STOP`, it cancels all pending orders managed by this EA.
### Order-Type Price Rules
- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry`
@@ -169,7 +172,7 @@ New entries are allowed only when all conditions below are satisfied:
## 7. Exit Conditions
- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from setup time.
- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from the H1 candidate time restored from the comment. Legacy orders without that token fall back to setup time.
- Positions are closed after `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from entry time.
- Time-limit handling runs early in `OnTick` and is independent of Python readiness or spread checks.
- On EA removal, `OnDeinit()` releases external-process state and the SLTP panel, then clears the chart status text with `Comment("")` and `ChartRedraw(0)`.
@@ -191,10 +194,13 @@ New entries are allowed only when all conditions below are satisfied:
- `input_position_limit` counts only this EA's orders and positions, not the full account. The hard internal limit is `POSITION_LIMIT`.
- The EA selects the filling policy from `SYMBOL_FILLING_MODE`, preferring IOC, then FOK, then RETURN.
- Before sending either single or split orders, the EA verifies the requested lot against `SYMBOL_VOLUME_MIN/MAX/STEP`.
- Pending orders receive server-side expiration when the broker supports it.
- Pending orders receive server-side expiration from the H1 candidate time when the broker supports it. Orders are skipped if that expiration is already in the past.
- `OrderSend` return values and `MqlTradeResult.retcode` are checked. Failures are logged with `GetLastError()` or the retcode.
- Split-entry order comments include `candidate_id` and slot number to prevent duplicate slot execution.
- Single and split order comments keep a valid `candidate_id` for candidate-origin expiration handling.
- Split-entry order comments also include order type and slot number to prevent duplicate execution of the same strategy slot.
- When `cancel_old_split_pending_on_new_zone = true`, a new valid `candidate_id` cancels older split pending orders, and an invalid or stopped `target_zones.txt` cancels all existing split pending orders for this EA/symbol/magic.
- After an H4 state update, existing pending orders that contradict the refreshed `market_state` are cancelled.
- A failed pending-order cancellation is not retried for the same ticket until `input_cancel_retry_cooldown_seconds` has elapsed, preventing excessive per-tick trade-server requests.
- SLTP management only targets existing positions matching `_Symbol` and `magic_number`, and it preserves each position's current TP.
- SL changes are sent only when the candidate improves protection relative to the current SL and satisfies broker stop-level and freeze-level constraints.
- `HighVolatilityLimit()` evaluates BUY positions with Bid and SELL positions with Ask, and treats GOLD/XAUUSD suffix symbols as 1 pip = 0.1.
@@ -203,13 +209,27 @@ New entries are allowed only when all conditions below are satisfied:
- If `trend_state.txt` is missing, unreadable, not strictly parseable as an integer, or outside `0..6`, the EA uses `MARKET_TECHNICAL_ERROR_STOP`.
- If `target_prices.txt` is missing, unreadable, shorter than 13 lines, or contains a non-strict numeric line, the EA uses `res_chk = 0`.
- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, or non-strict numeric values stop new split orders.
- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, a valid-zone `candidate_id` that is not a strict 12-digit timestamp, or non-strict numeric values stop new split orders.
- When Python falls back to H1 safe-stop values, it appends `RESULT: SAFE_STOP`, the stop stage, reason, H4 state, and candidate ID to `debug_entry.txt`. This distinguishes a normal GPT skip from API, configuration, or data failures.
- If a Python process exits with a non-zero code, the code is logged and the next trigger may retry.
- If a Python process remains active beyond `PYTHON_TIMEOUT_SECONDS`, the EA runs `taskkill /T /F` against the cmd/bat process tree, clears done/running files, and allows the next trigger to retry.
- If only a stale running marker remains, the EA attempts PID recovery. If recovery fails and the marker is timed out, it removes the stale marker.
## 10. Changelog
### 2026-06-01
- Added `debug_entry.txt` safe-stop diagnostics with the stop stage and reason so normal candidate skips can be distinguished from guarded failure fallbacks.
### 2026-05-31
- Changed startup initialization to rebuild H4/H1 results regardless of `initial_order`, while holding the technical-error stop state until the H4 result reloads.
- Added cancellation of existing pending orders that contradict a refreshed H4 `market_state`.
- Added per-ticket cooldown after failed pending-order cancellation to prevent excessive per-tick trade-server requests.
- Added order type to split-slot identifiers so different strategies using the same `candidate_id` and slot cannot collide.
- Kept valid `candidate_id` tokens in both single and split order comments and changed pending expiration to use the H1 candidate origin time.
- Required strict 12-digit timestamp `candidate_id` values for valid `target_zones.txt`; malformed values now stop safely.
### 2026-05-30
- Added the `HIT_<symbol>_<magic_number>` linkage-file prefix contract on both the MT5 and Python sides to avoid collisions across multiple EAs or symbols in the same terminal.
@@ -0,0 +1,259 @@
# HIT-EA Specification (English)
## 1. Overview
- This EA sends confirmed H4 OHLC data to an external Python process for market-state analysis and confirmed H1 OHLC data for entry candidate generation.
- The MQL5 side reads Python output files and places pending orders only after checking the H4 market state, H1 candidate prices, M15 confirmation filter, spread, and broker distance constraints.
- For backtest reproducibility, OHLC export uses `OHLC_START_SHIFT = 1`, so the forming bar is excluded and only closed bars are passed to Python.
- For live resilience, the EA tracks external-process running files, process IDs, exit codes, and timeouts to prevent duplicate launches and CSV overwrites.
## 2. Indicators
- No standard or custom indicators are used.
- OHLC data is retrieved with `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)`.
- M15 entry confirmation uses candle shape, average range, and distance to the candidate price for T2/T4. For T1/T3 it uses M15 impulse confirmation when that option is enabled.
## 3. Parameters
### input
- `lot_size = 0.01`: Order volume.
- `spread_limit = 60`: Maximum allowed spread in points.
- `magic_number = 10001`: Magic number used to identify this EA's orders and positions.
- `initial_order = 0`: Enables initial H4/H1 processing on startup when set to `1`.
- `input_position_limit = 10`: Practical limit for this EA's pending orders plus positions. Values above the internal `POSITION_LIMIT` are clamped.
- `use_split_entry_zone = false`: Enables split entries based on H1 predicted zones.
- `split_entry_count = 3`: Number of split entry orders, clamped to 1..10.
- `split_lot_mode = SPLIT_LOT_TOTAL`: Split lot mode. `SPLIT_LOT_TOTAL` divides the total lot across slots, and `SPLIT_LOT_FIXED` uses a fixed lot per order.
- `split_total_lot_size = 0.09`: Total lot used by total-split mode.
- `split_fixed_lot_size = 0.01`: Per-order lot used by fixed-split mode.
- `cancel_old_split_pending_on_new_zone = true`: Cancels older split pending orders when a new H1 zone set is loaded.
- `use_m15_entry_filter = true`: Enables M15 closed-bar entry timing confirmation.
- `m15_entry_zone_atr_multiplier = 1.50`: Allowed proximity multiplier based on the M15 average range.
- `use_m15_imbalance_confirmation = true`: Adds M15 impulse confirmation for T1/T3 trend-following candidates.
- `m15_imbalance_avg_body_period = 20`: Average-body period used by M15 impulse confirmation.
- `m15_imbalance_sensitivity = 2.0`: Required multiple of the average body for M15 impulse confirmation.
- `m15_imbalance_min_avg_body_points = 1.0`: Minimum M15 average body in points.
- `use_m15_imbalance_debug_log = false`: Enables detailed M15 impulse-confirmation logs.
- `input_entry_max_candidate_age_minutes = 120`: Maximum age, in minutes, for using an H1 candidate for new execution. This blocks delayed entries when M15 confirmation appears too late.
- `input_sltp_manager_enabled = false`: Enables UI-driven existing-position SL management through `CSLTPManager`.
- `input_sltp_show_panel = true`: Shows the SLTP control panel on the chart.
- `input_sltp_use_breakeven = false`: Enables normal break-even.
- `input_sltp_breakeven_trigger_pips = 30.0`: Profit threshold for normal break-even.
- `input_sltp_breakeven_buffer_pips = 3.0`: Profit-side entry buffer used by normal break-even.
- `input_sltp_use_elapsed_breakeven = false`: Enables elapsed-time break-even.
- `input_sltp_elapsed_breakeven_hours = 4.0`: Holding time required before elapsed-time break-even is allowed.
- `input_sltp_elapsed_breakeven_buffer_pips = 3.0`: Profit-side entry buffer used by elapsed-time break-even.
- `input_sltp_use_active_trailing = false`: Enables active step trailing.
- `input_sltp_active_breakeven_pips = 30.0`: Profit threshold that starts active trailing.
- `input_sltp_active_stop_loss_offset_pips = 5.0`: Initial locked profit offset from entry.
- `input_sltp_active_step_trigger_pips = 10.0`: Additional profit interval required for each SL step.
- `input_sltp_active_step_move_pips = 5.0`: SL movement added per completed step.
- `input_sltp_use_tp_progress_stop = false`: Enables TP-progress-based SL locking.
- `input_sltp_tp_progress_trigger_percent = 70.0`: TP progress required before the SL lock is applied.
- `input_sltp_tp_progress_sl_lock_percent = 30.0`: Entry-to-TP distance percentage locked by SL.
- `input_sltp_use_high_volatility_limit = false`: Enables short-term high-volatility SL tightening.
### Main Constants
- `POSITION_LIMIT = 48`: Maximum number of this EA's pending orders plus positions.
- `ENTRY_H1_LIMIT = 2`: Pending-order lifetime in H1 bars.
- `CLOSE_H1_LIMIT = 12`: Position lifetime in H1 bars.
- `HISTORY_BARS = 72`: Number of OHLC bars exported to Python.
- `OHLC_START_SHIFT = 1`: Shift used to export closed bars only.
- `M15_CONFIRM_BARS = 30`: Number of M15 bars used for average-range calculation.
- `ENTRY_RETRY_SECONDS = 60`: Retry interval for entry checks.
- `ENTRY_RETRY_LIMIT = 10`: Maximum retry count for one H1 candidate set.
- `TARGET_SIZE = 13`: Number of numeric values expected in `target_prices.txt`.
- `TARGET_ZONE_SCHEMA_VERSION = 2`: Schema version used by `target_zones.txt`.
- `PYTHON_TIMEOUT_SECONDS = 600`: External Python wait threshold.
### Python Entry-Candidate Guard
- After H1 candidate generation, Python invalidates any `entry` that is too far from the current price.
- The maximum distance is strategy-specific: T1/T3 Stop candidates use `max(H1 EATR * 1.50, 5.00)`, and T2/T4 Limit candidates use `max(H1 EATR * 1.00, 5.00)`. A strategy beyond its limit is rewritten to `0.00,0.00,0.00`.
- This post-filter is intended to reduce delayed entries from deep limit orders or distant breakout waits after M15 confirmation arrives late.
## 4. File Layout
- `Experts/MyProject/HIT-EA_refactor_ver6.mq5`: EA entry point. Keeps `OnInit`, `OnDeinit`, `OnTick`, inputs, global state, and DLL imports.
- `Include/MyLib/Common/HITRuntimeController.mqh`: Tick flow, H4/H1/M15 update control, status comments, and spread checks.
- `Include/MyLib/Signals/HITEntrySignal.mqh`: Entry preconditions, order-type price validation, M15 filter, and retry-state updates.
- `Include/MyLib/Common/HITExternalProcess.mqh`: done/running files, external process handles, PID recovery, exit-code checks, and timeout recovery.
- `Include/MyLib/Signals/HITPythonSignalGateway.mqh`: OHLC CSV export, Python batch launch, and `trend_state.txt` / `target_prices.txt` / `target_zones.txt` loading.
- `Include/MyLib/Trading/HITTradeManager.mqh`: Order sending, filling policy, pending-order expiration, order/position counting, expired order cancellation, and expired position closing.
- `Include/MyLib/Trading/SLTPManager.mqh`: Existing-position break-even, elapsed-time break-even, active trailing, TP-progress SL, and high-volatility SL management.
- `Include/MyLib/Panel/SLTPManagerPanel.mqh`: Chart panel that toggles `CSLTPManager` features, validates numeric inputs, and applies settings to the EA.
## 5. Python Integration Files
During `OnInit()`, the EA builds a `HIT_<sanitized symbol>_<magic_number>` prefix from `_Symbol` and `magic_number`, then applies it to all MT5 `Files` linkage names. Example: `HIT_GOLD_10001_ohlc_H4.csv`. The same prefix is passed to the batch file as the first argument, and the batch file exposes it to Python as `MT5_EA_FILE_PREFIX`. The EA also passes the symbol's `SYMBOL_DIGITS` as the second argument, exposed to Python as `MT5_PRICE_DIGITS`.
When no prefix is provided, such as manual Python execution, Python keeps using the legacy names like `ohlc_H4.csv`.
### H4 Trend Analysis
- MQL5 output: `<prefix>_ohlc_H4.csv`
- Python output: `<prefix>_trend_state.txt`
- Done marker: `<prefix>_process_done_trend.txt`
- Running marker: `<prefix>_process_running_trend.txt`
- Batch file: `<TerminalDataPath>\MQL5\python_for_ea\bat\get_trend_reply.bat`
### H1 Entry Candidate Generation
- MQL5 output: `<prefix>_ohlc_H1.csv`
- Python output: `<prefix>_target_prices.txt`, `<prefix>_target_zones.txt`
- Done marker: `<prefix>_process_done_entry.txt`
- Running marker: `<prefix>_process_running_entry.txt`
- 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:
```text
Line 1: res_chk
Lines 2-4: T1 Buy Stop entry/tp/sl
Lines 5-7: T2 Buy Limit entry/tp/sl
Lines 8-10: T3 Sell Stop entry/tp/sl
Lines 11-13: T4 Sell Limit entry/tp/sl
```
`target_zones.txt` must contain 7 lines for split entries:
```text
Line 1: schema_version (2)
Line 2: res_chk
Line 3: candidate_id derived from the closed H1 bar time
Line 4: T1 Buy Stop strategy, zone_low, zone_high, tp, sl
Line 5: T2 Buy Limit strategy, zone_low, zone_high, tp, sl
Line 6: T3 Sell Stop strategy, zone_low, zone_high, tp, sl
Line 7: T4 Sell Limit strategy, zone_low, zone_high, tp, sl
```
Python creates `process_done_entry.txt` only after both `target_prices.txt` and `target_zones.txt` have been written. Prices in `target_zones.txt` are formatted with `MT5_PRICE_DIGITS`; manual runs without this value use a conservative 5 decimal places. When split entries are disabled, the EA keeps using the legacy 13-line `target_prices.txt` path.
## 6. Entry Conditions
New entries are allowed only when all conditions below are satisfied:
- Spread is less than or equal to `spread_limit`.
- H4 trend analysis is complete and `trend_state.txt` can be loaded.
- H1 entry candidate generation is complete and `target_prices.txt` can be loaded.
- `res_chk = 1`.
- The H1 candidate set has not expired under `ENTRY_H1_LIMIT`, measured from the closed H1 bar time that produced the candidate.
- The H1 candidate set has not exceeded `input_entry_max_candidate_age_minutes`, measured from the closed H1 bar time that produced the candidate.
- The selected order type has valid `entry/tp/sl` values greater than `0.0`.
- The order type is allowed by the H4 `market_state`.
- The order-type price relationship and broker minimum distance rules are satisfied.
- When `use_m15_entry_filter = true`, T2/T4 satisfy the M15 direction, rejection, and proximity checks. T1/T3 satisfy M15 impulse confirmation when `use_m15_imbalance_confirmation = true`.
- This EA's pending orders plus positions are below `input_position_limit`.
- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + slot.
### market_state and Allowed Order Types
- `0 MARKET_LOW_VOL_RANGE`: Allows T2 Buy Limit / T4 Sell Limit.
- `1 MARKET_HIGH_VOL_RANGE`: Allows T2 Buy Limit / T4 Sell Limit.
- `2 MARKET_LOW_VOL_UP`: Allows T1 Buy Stop / T2 Buy Limit.
- `3 MARKET_HIGH_VOL_UP`: Allows T1 Buy Stop / T2 Buy Limit.
- `4 MARKET_LOW_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit.
- `5 MARKET_HIGH_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit.
- `6 MARKET_TECHNICAL_ERROR_STOP`: Blocks new entries.
### Order-Type Price Rules
- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry`
- T2 Buy Limit: `Ask > entry`, `tp > entry`, `sl < entry`
- T3 Sell Stop: `Bid > entry`, `tp < entry`, `sl > entry`
- T4 Sell Limit: `Bid < entry`, `tp < entry`, `sl > entry`
## 7. Exit Conditions
- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from setup time.
- Positions are closed after `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from entry time.
- Time-limit handling runs early in `OnTick` and is independent of Python readiness or spread checks.
- On EA removal, `OnDeinit()` releases external-process state and the SLTP panel, then clears the chart status text with `Comment("")` and `ChartRedraw(0)`.
- When `input_sltp_manager_enabled` or the panel `Manager` toggle is ON, the EA runs `CSLTPManager::ManagePositions()` and `HighVolatilityLimit()` after time-limit handling. Existing-position SL protection therefore continues while Python results are pending or new entries are skipped.
- Normal break-even and active trailing are mutually exclusive. The panel turns one mode off when the other is enabled, and `CSLTPManager::ValidateSettings()` also validates the exclusion.
## 7.1 SLTP Control Panel
- The panel is created when `input_sltp_show_panel = true` and appears near the chart's upper-right area.
- If panel creation fails, the EA logs the failure and continues with the already-applied input-based SLTP settings.
- The `Manager` button toggles all SLTP management. When OFF, panel values remain stored but `OnTick` does not modify SL values.
- The `BreakEven`, `Elapsed BE`, `Active Trail`, `TP Progress`, and `High Vol` buttons toggle each `CSLTPManager` feature.
- Numeric fields are validated when `APPLY` is pressed. Invalid or out-of-range values show a `MessageBox`, are logged, and revert to the previous valid value.
- Settings are copied into `CSLTPManager` only after `APPLY` succeeds. The EA then calls `ValidateSettings()`; if validation fails, SLTP management is stopped and the reason is logged.
## 8. Risk Management
- Managed orders and positions are limited to matching `_Symbol` and `magic_number`.
- `input_position_limit` counts only this EA's orders and positions, not the full account. The hard internal limit is `POSITION_LIMIT`.
- The EA selects the filling policy from `SYMBOL_FILLING_MODE`, preferring IOC, then FOK, then RETURN.
- Before sending either single or split orders, the EA verifies the requested lot against `SYMBOL_VOLUME_MIN/MAX/STEP`.
- Pending orders receive server-side expiration when the broker supports it.
- `OrderSend` return values and `MqlTradeResult.retcode` are checked. Failures are logged with `GetLastError()` or the retcode.
- Split-entry order comments include `candidate_id` and slot number to prevent duplicate slot execution.
- When `cancel_old_split_pending_on_new_zone = true`, a new valid `candidate_id` cancels older split pending orders, and an invalid or stopped `target_zones.txt` cancels all existing split pending orders for this EA/symbol/magic.
- SLTP management only targets existing positions matching `_Symbol` and `magic_number`, and it preserves each position's current TP.
- SL changes are sent only when the candidate improves protection relative to the current SL and satisfies broker stop-level and freeze-level constraints.
- `HighVolatilityLimit()` evaluates BUY positions with Bid and SELL positions with Ask, and treats GOLD/XAUUSD suffix symbols as 1 pip = 0.1.
## 9. Error Handling
- If `trend_state.txt` is missing, unreadable, not strictly parseable as an integer, or outside `0..6`, the EA uses `MARKET_TECHNICAL_ERROR_STOP`.
- If `target_prices.txt` is missing, unreadable, shorter than 13 lines, or contains a non-strict numeric line, the EA uses `res_chk = 0`.
- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, or non-strict numeric values stop new split orders.
- If a Python process exits with a non-zero code, the code is logged and the next trigger may retry.
- If a Python process remains active beyond `PYTHON_TIMEOUT_SECONDS`, the EA runs `taskkill /T /F` against the cmd/bat process tree, clears done/running files, and allows the next trigger to retry.
- If only a stale running marker remains, the EA attempts PID recovery. If recovery fails and the marker is timed out, it removes the stale marker.
## 10. Changelog
### 2026-05-30
- Added the `HIT_<symbol>_<magic_number>` linkage-file prefix contract on both the MT5 and Python sides to avoid collisions across multiple EAs or symbols in the same terminal.
- Updated T1/T3 trend-following candidates so `use_m15_imbalance_confirmation = true` requires M15 impulse confirmation.
- Added strict numeric parsing requirements for `trend_state.txt`, `target_prices.txt`, and `target_zones.txt`, with invalid content handled as a safe stop state.
- Added stale split-pending cancellation behavior when `target_zones.txt` is invalid or stopped.
- Synchronized current defaults for `input_entry_max_candidate_age_minutes`, `ENTRY_H1_LIMIT`, SLTP buffers, and the Python entry-distance guard.
- Added process-tree termination and retry recovery for Python timeouts.
- Changed H1 candidate freshness checks to use the closed H1 bar time restored from `candidate_id`, instead of the Python result load time.
- Added `MT5_PRICE_DIGITS` so Python formats `target_zones.txt` prices with the active symbol's decimal precision.
- Added single-order lot validation, OHLC CSV NaN/inf and consistency checks, Ask-based SELL high-volatility SL evaluation, and GOLD/XAUUSD suffix handling.
### 2026-05-27
- Moved the Python helper application specification from `C:\ea_py` to `MQL5\python_for_ea` under the MT5 data folder.
- Updated the EA startup path model so batch paths are built from `TerminalInfoString(TERMINAL_DATA_PATH)`, and the external process working directory is derived from the batch-file location.
- Updated Python path discovery so the default MT5 linkage is the adjacent `MQL5\Files` directory, with `MT5_FILES_DIR` or `MT5_DATA_PATH` available for explicit overrides.
### 2026-05-24
- Added Python-side `target_zones.txt` output and made `process_done_entry.txt` appear only after both `target_prices.txt` and `target_zones.txt` are written.
- Added `use_split_entry_zone`, `split_entry_count`, `split_lot_mode`, `split_total_lot_size`, `split_fixed_lot_size`, and `input_position_limit`.
- Added split-zone pending order placement with total-lot or fixed-lot sizing.
- Added `candidate_id` + slot identification to prevent duplicate split orders.
### 2026-05-13
- Added a Python-side H1 EATR distance guard that invalidates candidates too far from the current price before they are passed to MT5.
- Added `input_entry_max_candidate_age_minutes` on the EA side so stale H1 candidates are not executed when M15 confirmation arrives late.
- Added H1 candidate age to entry skip logs to make delayed entries, M15 rejection, and price mismatch easier to diagnose.
### 2026-05-05
- Added `Comment("")` and `ChartRedraw(0)` to `OnDeinit()` so the chart status comment is removed when the EA is deleted.
- Connected `CSLTPManager` and the dedicated chart panel `SLTPManagerPanel.mqh` to `HIT-EA_refactor_ver5.mq5`, allowing UI control of break-even, elapsed-time break-even, active trailing, TP-progress SL, and high-volatility SL tightening.
- Added SLTP management immediately after time-limit handling in `OnTick`, so existing-position protection continues independently of Python readiness and spread checks.
- Added SLTP panel inputs, mutual exclusion for normal break-even versus active trailing, and numeric validation on `APPLY`.
- Split the function implementations from `HIT-EA_refactor_ver5.mq5` into purpose-specific `.mqh` files.
- Kept `OnInit`, `OnDeinit`, `OnTick`, inputs, and global state in the EA source while moving behavior functions under `Include/MyLib/`.
- Updated the specification to match the current `CreateProcessW` Python process tracking, M15 filter, `market_state` range `0..6`, and `spread_limit = 60`.
### 2026-05-02
- Added entry suppression when `res_chk = 0`.
- Moved time-limit handling before Python readiness and spread checks.
- Split startup flags for H4 and H1 initial processing.
- Switched OHLC retrieval to `CopyRates` and exported closed bars only.
@@ -0,0 +1,274 @@
# HIT-EA Specification (English)
## 1. Overview
- This EA sends confirmed H4 OHLC data to an external Python process for market-state analysis and confirmed H1 OHLC data for entry candidate generation.
- The MQL5 side reads Python output files and places pending orders only after checking the H4 market state, H1 candidate prices, M15 confirmation filter, spread, and broker distance constraints.
- For backtest reproducibility, OHLC export uses `OHLC_START_SHIFT = 1`, so the forming bar is excluded and only closed bars are passed to Python.
- For live resilience, the EA tracks external-process running files, process IDs, exit codes, and timeouts to prevent duplicate launches and CSV overwrites.
## 2. Indicators
- No standard or custom indicators are used.
- OHLC data is retrieved with `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)`.
- M15 entry confirmation uses candle shape, average range, and distance to the candidate price for T2/T4. For T1/T3 it uses M15 impulse confirmation when that option is enabled.
## 3. Parameters
### input
- `lot_size = 0.01`: Order volume.
- `spread_limit = 60`: Maximum allowed spread in points.
- `magic_number = 10001`: Magic number used to identify this EA's orders and positions.
- `initial_order = 0`: Retained for compatibility with older settings. Startup always rebuilds H4/H1 results and remains in technical-error stop state until the H4 result is reloaded.
- `input_position_limit = 10`: Practical limit for this EA's pending orders plus positions. Values above the internal `POSITION_LIMIT` are clamped.
- `use_split_entry_zone = false`: Enables split entries based on H1 predicted zones.
- `split_entry_count = 3`: Number of split entry orders, clamped to 1..10.
- `split_lot_mode = SPLIT_LOT_TOTAL`: Split lot mode. `SPLIT_LOT_TOTAL` divides the total lot across slots, and `SPLIT_LOT_FIXED` uses a fixed lot per order.
- `split_total_lot_size = 0.09`: Total lot used by total-split mode.
- `split_fixed_lot_size = 0.01`: Per-order lot used by fixed-split mode.
- `cancel_old_split_pending_on_new_zone = true`: Cancels older split pending orders when a new H1 zone set is loaded.
- `input_cancel_retry_cooldown_seconds = 60`: Wait time before retrying a failed pending-order cancellation for the same ticket. The effective value is clamped to 1..3600 seconds.
- `use_m15_entry_filter = true`: Enables M15 closed-bar entry timing confirmation.
- `m15_entry_zone_atr_multiplier = 1.50`: Allowed proximity multiplier based on the M15 average range.
- `use_m15_imbalance_confirmation = true`: Adds M15 impulse confirmation for T1/T3 trend-following candidates.
- `m15_imbalance_avg_body_period = 20`: Average-body period used by M15 impulse confirmation.
- `m15_imbalance_sensitivity = 2.0`: Required multiple of the average body for M15 impulse confirmation.
- `m15_imbalance_min_avg_body_points = 1.0`: Minimum M15 average body in points.
- `use_m15_imbalance_debug_log = false`: Enables detailed M15 impulse-confirmation logs.
- `input_entry_max_candidate_age_minutes = 120`: Maximum age, in minutes, for using an H1 candidate for new execution. This blocks delayed entries when M15 confirmation appears too late.
- `input_sltp_manager_enabled = false`: Enables UI-driven existing-position SL management through `CSLTPManager`.
- `input_sltp_show_panel = true`: Shows the SLTP control panel on the chart.
- `input_sltp_use_breakeven = false`: Enables normal break-even.
- `input_sltp_breakeven_trigger_pips = 30.0`: Profit threshold for normal break-even.
- `input_sltp_breakeven_buffer_pips = 3.0`: Profit-side entry buffer used by normal break-even.
- `input_sltp_use_elapsed_breakeven = false`: Enables elapsed-time break-even.
- `input_sltp_elapsed_breakeven_hours = 4.0`: Holding time required before elapsed-time break-even is allowed.
- `input_sltp_elapsed_breakeven_buffer_pips = 3.0`: Profit-side entry buffer used by elapsed-time break-even.
- `input_sltp_use_active_trailing = false`: Enables active step trailing.
- `input_sltp_active_breakeven_pips = 30.0`: Profit threshold that starts active trailing.
- `input_sltp_active_stop_loss_offset_pips = 5.0`: Initial locked profit offset from entry.
- `input_sltp_active_step_trigger_pips = 10.0`: Additional profit interval required for each SL step.
- `input_sltp_active_step_move_pips = 5.0`: SL movement added per completed step.
- `input_sltp_use_tp_progress_stop = false`: Enables TP-progress-based SL locking.
- `input_sltp_tp_progress_trigger_percent = 70.0`: TP progress required before the SL lock is applied.
- `input_sltp_tp_progress_sl_lock_percent = 30.0`: Entry-to-TP distance percentage locked by SL.
- `input_sltp_use_high_volatility_limit = false`: Enables short-term high-volatility SL tightening.
### Main Constants
- `POSITION_LIMIT = 48`: Maximum number of this EA's pending orders plus positions.
- `ENTRY_H1_LIMIT = 2`: Pending-order lifetime in H1 bars.
- `CLOSE_H1_LIMIT = 12`: Position lifetime in H1 bars.
- `HISTORY_BARS = 72`: Number of OHLC bars exported to Python.
- `OHLC_START_SHIFT = 1`: Shift used to export closed bars only.
- `M15_CONFIRM_BARS = 30`: Number of M15 bars used for average-range calculation.
- `ENTRY_RETRY_SECONDS = 60`: Retry interval for entry checks.
- `ENTRY_RETRY_LIMIT = 10`: Maximum retry count for one H1 candidate set.
- `TARGET_SIZE = 13`: Number of numeric values expected in `target_prices.txt`.
- `TARGET_ZONE_SCHEMA_VERSION = 2`: Schema version used by `target_zones.txt`.
- `PYTHON_TIMEOUT_SECONDS = 600`: External Python wait threshold.
### Python Entry-Candidate Guard
- After H1 candidate generation, Python invalidates any `entry` that is too far from the current price.
- The maximum distance is strategy-specific: T1/T3 Stop candidates use `max(H1 EATR * 1.50, 5.00)`, and T2/T4 Limit candidates use `max(H1 EATR * 1.00, 5.00)`. A strategy beyond its limit is rewritten to `0.00,0.00,0.00`.
- This post-filter is intended to reduce delayed entries from deep limit orders or distant breakout waits after M15 confirmation arrives late.
## 4. File Layout
- `Experts/MyProject/HIT-EA_refactor_ver6.mq5`: EA entry point. Keeps `OnInit`, `OnDeinit`, `OnTick`, inputs, global state, and DLL imports.
- `Include/MyLib/Common/HITRuntimeController.mqh`: Tick flow, H4/H1/M15 update control, status comments, and spread checks.
- `Include/MyLib/Signals/HITEntrySignal.mqh`: Entry preconditions, order-type price validation, M15 filter, and retry-state updates.
- `Include/MyLib/Common/HITExternalProcess.mqh`: done/running files, external process handles, PID recovery, exit-code checks, and timeout recovery.
- `Include/MyLib/Signals/HITPythonSignalGateway.mqh`: OHLC CSV export, Python batch launch, and `trend_state.txt` / `target_prices.txt` / `target_zones.txt` loading.
- `Include/MyLib/Trading/HITTradeManager.mqh`: Order sending, filling policy, pending-order expiration, order/position counting, expired order cancellation, and expired position closing.
- `Include/MyLib/Trading/SLTPManager.mqh`: Existing-position break-even, elapsed-time break-even, active trailing, TP-progress SL, and high-volatility SL management.
- `Include/MyLib/Panel/SLTPManagerPanel.mqh`: Chart panel that toggles `CSLTPManager` features, validates numeric inputs, and applies settings to the EA.
## 5. Python Integration Files
During `OnInit()`, the EA builds a `HIT_<sanitized symbol>_<magic_number>` prefix from `_Symbol` and `magic_number`, then applies it to all MT5 `Files` linkage names. Example: `HIT_GOLD_10001_ohlc_H4.csv`. The same prefix is passed to the batch file as the first argument, and the batch file exposes it to Python as `MT5_EA_FILE_PREFIX`. The EA also passes the symbol's `SYMBOL_DIGITS` as the second argument, exposed to Python as `MT5_PRICE_DIGITS`.
When no prefix is provided, such as manual Python execution, Python keeps using the legacy names like `ohlc_H4.csv`.
### H4 Trend Analysis
- MQL5 output: `<prefix>_ohlc_H4.csv`
- Python output: `<prefix>_trend_state.txt`
- Done marker: `<prefix>_process_done_trend.txt`
- Running marker: `<prefix>_process_running_trend.txt`
- Batch file: `<TerminalDataPath>\MQL5\python_for_ea\bat\get_trend_reply.bat`
### H1 Entry Candidate Generation
- MQL5 output: `<prefix>_ohlc_H1.csv`
- Python output: `<prefix>_target_prices.txt`, `<prefix>_target_zones.txt`
- Done marker: `<prefix>_process_done_entry.txt`
- Running marker: `<prefix>_process_running_entry.txt`
- 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:
```text
Line 1: res_chk
Lines 2-4: T1 Buy Stop entry/tp/sl
Lines 5-7: T2 Buy Limit entry/tp/sl
Lines 8-10: T3 Sell Stop entry/tp/sl
Lines 11-13: T4 Sell Limit entry/tp/sl
```
`target_zones.txt` must contain 7 lines for split entries:
```text
Line 1: schema_version (2)
Line 2: res_chk
Line 3: candidate_id derived from the closed H1 bar time
Line 4: T1 Buy Stop strategy, zone_low, zone_high, tp, sl
Line 5: T2 Buy Limit strategy, zone_low, zone_high, tp, sl
Line 6: T3 Sell Stop strategy, zone_low, zone_high, tp, sl
Line 7: T4 Sell Limit strategy, zone_low, zone_high, tp, sl
```
Python creates `process_done_entry.txt` only after both `target_prices.txt` and `target_zones.txt` have been written. Prices in `target_zones.txt` are formatted with `MT5_PRICE_DIGITS`; manual runs without this value use a conservative 5 decimal places. When split entries are disabled, the EA keeps using the legacy 13-line `target_prices.txt` path.
## 6. Entry Conditions
New entries are allowed only when all conditions below are satisfied:
- Spread is less than or equal to `spread_limit`.
- H4 trend analysis is complete and `trend_state.txt` can be loaded.
- H1 entry candidate generation is complete and `target_prices.txt` can be loaded.
- `res_chk = 1`.
- The H1 candidate set has not expired under `ENTRY_H1_LIMIT`, measured from the closed H1 bar time that produced the candidate.
- The H1 candidate set has not exceeded `input_entry_max_candidate_age_minutes`, measured from the closed H1 bar time that produced the candidate.
- The selected order type has valid `entry/tp/sl` values greater than `0.0`.
- The order type is allowed by the H4 `market_state`.
- The order-type price relationship and broker minimum distance rules are satisfied.
- When `use_m15_entry_filter = true`, T2/T4 satisfy the M15 direction, rejection, and proximity checks. T1/T3 satisfy M15 impulse confirmation when `use_m15_imbalance_confirmation = true`.
- This EA's pending orders plus positions are below `input_position_limit`.
- When split entries are enabled, `target_zones.txt` has `res_chk = 1`, the predicted zone prices are valid, split lots fit `SYMBOL_VOLUME_MIN/MAX/STEP`, and no duplicate order or position exists for the same `candidate_id` + order type + slot.
### market_state and Allowed Order Types
- `0 MARKET_LOW_VOL_RANGE`: Allows T2 Buy Limit / T4 Sell Limit.
- `1 MARKET_HIGH_VOL_RANGE`: Allows T2 Buy Limit / T4 Sell Limit.
- `2 MARKET_LOW_VOL_UP`: Allows T1 Buy Stop / T2 Buy Limit.
- `3 MARKET_HIGH_VOL_UP`: Allows T1 Buy Stop / T2 Buy Limit.
- `4 MARKET_LOW_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit.
- `5 MARKET_HIGH_VOL_DOWN`: Allows T3 Sell Stop / T4 Sell Limit.
- `6 MARKET_TECHNICAL_ERROR_STOP`: Blocks new entries.
After an H4 `market_state` reload, the EA cancels existing pending orders whose order types are not allowed by the refreshed state. For `6 MARKET_TECHNICAL_ERROR_STOP`, it cancels all pending orders managed by this EA.
### Order-Type Price Rules
- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry`
- T2 Buy Limit: `Ask > entry`, `tp > entry`, `sl < entry`
- T3 Sell Stop: `Bid > entry`, `tp < entry`, `sl > entry`
- T4 Sell Limit: `Bid < entry`, `tp < entry`, `sl > entry`
## 7. Exit Conditions
- Pending orders are cancelled after `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from the H1 candidate time restored from the comment. Legacy orders without that token fall back to setup time.
- Positions are closed after `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` has elapsed from entry time.
- Time-limit handling runs early in `OnTick` and is independent of Python readiness or spread checks.
- On EA removal, `OnDeinit()` releases external-process state and the SLTP panel, then clears the chart status text with `Comment("")` and `ChartRedraw(0)`.
- When `input_sltp_manager_enabled` or the panel `Manager` toggle is ON, the EA runs `CSLTPManager::ManagePositions()` and `HighVolatilityLimit()` after time-limit handling. Existing-position SL protection therefore continues while Python results are pending or new entries are skipped.
- Normal break-even and active trailing are mutually exclusive. The panel turns one mode off when the other is enabled, and `CSLTPManager::ValidateSettings()` also validates the exclusion.
## 7.1 SLTP Control Panel
- The panel is created when `input_sltp_show_panel = true` and appears near the chart's upper-right area.
- If panel creation fails, the EA logs the failure and continues with the already-applied input-based SLTP settings.
- The `Manager` button toggles all SLTP management. When OFF, panel values remain stored but `OnTick` does not modify SL values.
- The `BreakEven`, `Elapsed BE`, `Active Trail`, `TP Progress`, and `High Vol` buttons toggle each `CSLTPManager` feature.
- Numeric fields are validated when `APPLY` is pressed. Invalid or out-of-range values show a `MessageBox`, are logged, and revert to the previous valid value.
- Settings are copied into `CSLTPManager` only after `APPLY` succeeds. The EA then calls `ValidateSettings()`; if validation fails, SLTP management is stopped and the reason is logged.
## 8. Risk Management
- Managed orders and positions are limited to matching `_Symbol` and `magic_number`.
- `input_position_limit` counts only this EA's orders and positions, not the full account. The hard internal limit is `POSITION_LIMIT`.
- The EA selects the filling policy from `SYMBOL_FILLING_MODE`, preferring IOC, then FOK, then RETURN.
- Before sending either single or split orders, the EA verifies the requested lot against `SYMBOL_VOLUME_MIN/MAX/STEP`.
- Pending orders receive server-side expiration from the H1 candidate time when the broker supports it. Orders are skipped if that expiration is already in the past.
- `OrderSend` return values and `MqlTradeResult.retcode` are checked. Failures are logged with `GetLastError()` or the retcode.
- Single and split order comments keep a valid `candidate_id` for candidate-origin expiration handling.
- Split-entry order comments also include order type and slot number to prevent duplicate execution of the same strategy slot.
- When `cancel_old_split_pending_on_new_zone = true`, a new valid `candidate_id` cancels older split pending orders, and an invalid or stopped `target_zones.txt` cancels all existing split pending orders for this EA/symbol/magic.
- After an H4 state update, existing pending orders that contradict the refreshed `market_state` are cancelled.
- A failed pending-order cancellation is not retried for the same ticket until `input_cancel_retry_cooldown_seconds` has elapsed, preventing excessive per-tick trade-server requests.
- SLTP management only targets existing positions matching `_Symbol` and `magic_number`, and it preserves each position's current TP.
- SL changes are sent only when the candidate improves protection relative to the current SL and satisfies broker stop-level and freeze-level constraints.
- `HighVolatilityLimit()` evaluates BUY positions with Bid and SELL positions with Ask, and treats GOLD/XAUUSD suffix symbols as 1 pip = 0.1.
## 9. Error Handling
- If `trend_state.txt` is missing, unreadable, not strictly parseable as an integer, or outside `0..6`, the EA uses `MARKET_TECHNICAL_ERROR_STOP`.
- If `target_prices.txt` is missing, unreadable, shorter than 13 lines, or contains a non-strict numeric line, the EA uses `res_chk = 0`.
- When split entries are enabled, missing `target_zones.txt`, schema mismatch, fewer than 7 lines, `res_chk != 1`, a valid-zone `candidate_id` that is not a strict 12-digit timestamp, or non-strict numeric values stop new split orders.
- If a Python process exits with a non-zero code, the code is logged and the next trigger may retry.
- If a Python process remains active beyond `PYTHON_TIMEOUT_SECONDS`, the EA runs `taskkill /T /F` against the cmd/bat process tree, clears done/running files, and allows the next trigger to retry.
- If only a stale running marker remains, the EA attempts PID recovery. If recovery fails and the marker is timed out, it removes the stale marker.
## 10. Changelog
### 2026-05-31
- Changed startup initialization to rebuild H4/H1 results regardless of `initial_order`, while holding the technical-error stop state until the H4 result reloads.
- Added cancellation of existing pending orders that contradict a refreshed H4 `market_state`.
- Added per-ticket cooldown after failed pending-order cancellation to prevent excessive per-tick trade-server requests.
- Added order type to split-slot identifiers so different strategies using the same `candidate_id` and slot cannot collide.
- Kept valid `candidate_id` tokens in both single and split order comments and changed pending expiration to use the H1 candidate origin time.
- Required strict 12-digit timestamp `candidate_id` values for valid `target_zones.txt`; malformed values now stop safely.
### 2026-05-30
- Added the `HIT_<symbol>_<magic_number>` linkage-file prefix contract on both the MT5 and Python sides to avoid collisions across multiple EAs or symbols in the same terminal.
- Updated T1/T3 trend-following candidates so `use_m15_imbalance_confirmation = true` requires M15 impulse confirmation.
- Added strict numeric parsing requirements for `trend_state.txt`, `target_prices.txt`, and `target_zones.txt`, with invalid content handled as a safe stop state.
- Added stale split-pending cancellation behavior when `target_zones.txt` is invalid or stopped.
- Synchronized current defaults for `input_entry_max_candidate_age_minutes`, `ENTRY_H1_LIMIT`, SLTP buffers, and the Python entry-distance guard.
- Added process-tree termination and retry recovery for Python timeouts.
- Changed H1 candidate freshness checks to use the closed H1 bar time restored from `candidate_id`, instead of the Python result load time.
- Added `MT5_PRICE_DIGITS` so Python formats `target_zones.txt` prices with the active symbol's decimal precision.
- Added single-order lot validation, OHLC CSV NaN/inf and consistency checks, Ask-based SELL high-volatility SL evaluation, and GOLD/XAUUSD suffix handling.
### 2026-05-27
- Moved the Python helper application specification from `C:\ea_py` to `MQL5\python_for_ea` under the MT5 data folder.
- Updated the EA startup path model so batch paths are built from `TerminalInfoString(TERMINAL_DATA_PATH)`, and the external process working directory is derived from the batch-file location.
- Updated Python path discovery so the default MT5 linkage is the adjacent `MQL5\Files` directory, with `MT5_FILES_DIR` or `MT5_DATA_PATH` available for explicit overrides.
### 2026-05-24
- Added Python-side `target_zones.txt` output and made `process_done_entry.txt` appear only after both `target_prices.txt` and `target_zones.txt` are written.
- Added `use_split_entry_zone`, `split_entry_count`, `split_lot_mode`, `split_total_lot_size`, `split_fixed_lot_size`, and `input_position_limit`.
- Added split-zone pending order placement with total-lot or fixed-lot sizing.
- Added `candidate_id` + slot identification to prevent duplicate split orders.
### 2026-05-13
- Added a Python-side H1 EATR distance guard that invalidates candidates too far from the current price before they are passed to MT5.
- Added `input_entry_max_candidate_age_minutes` on the EA side so stale H1 candidates are not executed when M15 confirmation arrives late.
- Added H1 candidate age to entry skip logs to make delayed entries, M15 rejection, and price mismatch easier to diagnose.
### 2026-05-05
- Added `Comment("")` and `ChartRedraw(0)` to `OnDeinit()` so the chart status comment is removed when the EA is deleted.
- Connected `CSLTPManager` and the dedicated chart panel `SLTPManagerPanel.mqh` to `HIT-EA_refactor_ver5.mq5`, allowing UI control of break-even, elapsed-time break-even, active trailing, TP-progress SL, and high-volatility SL tightening.
- Added SLTP management immediately after time-limit handling in `OnTick`, so existing-position protection continues independently of Python readiness and spread checks.
- Added SLTP panel inputs, mutual exclusion for normal break-even versus active trailing, and numeric validation on `APPLY`.
- Split the function implementations from `HIT-EA_refactor_ver5.mq5` into purpose-specific `.mqh` files.
- Kept `OnInit`, `OnDeinit`, `OnTick`, inputs, and global state in the EA source while moving behavior functions under `Include/MyLib/`.
- Updated the specification to match the current `CreateProcessW` Python process tracking, M15 filter, `market_state` range `0..6`, and `spread_limit = 60`.
### 2026-05-02
- Added entry suppression when `res_chk = 0`.
- Moved time-limit handling before Python readiness and spread checks.
- Split startup flags for H4 and H1 initial processing.
- Switched OHLC retrieval to `CopyRates` and exported closed bars only.
+26 -6
View File
@@ -20,7 +20,7 @@
- `lot_size = 0.01`: 注文ロット。
- `spread_limit = 60`: 許容スプレッド(point)。
- `magic_number = 10001`: EA識別用マジックナンバー。
- `initial_order = 0`: 起動直後の初回H4/H1処理を実行するか。`1` で有効
- `initial_order = 0`: 旧設定ファイルとの互換性維持用。値にかかわらず起動時はH4/H1を再生成し、H4再読込までは技術エラー停止状態を維持する
- `use_m15_entry_filter = true`: M15確定足によるエントリータイミング確認を使用するか。
- `m15_entry_zone_atr_multiplier = 1.50`: M15平均レンジに対する候補価格接近許容倍率。
- `use_m15_imbalance_confirmation = true`: T1/T3の順張り候補に対して、M15確定足の初動確認を追加するか。
@@ -36,6 +36,7 @@
- `split_total_lot_size = 0.09`: 総量分割モードの合計ロット。
- `split_fixed_lot_size = 0.01`: 固定ロットモードの1注文ロット。
- `cancel_old_split_pending_on_new_zone = true`: 新しいH1ゾーン読込時に、同じEAの古い分割pending注文を取消するか。
- `input_cancel_retry_cooldown_seconds = 60`: pending注文の取消に失敗した場合、同一ticketへ取消要求を再送するまでの待機秒数。1〜3600秒に丸めて使用する。
- `input_sltp_manager_enabled = false`: UI連動の `CSLTPManager` による既存ポジションSL管理を有効化するか。
- `input_sltp_show_panel = true`: チャート上にSLTP操作パネルを表示するか。
- `input_sltp_use_breakeven = false`: 通常ブレークイーブンを有効化するか。
@@ -148,7 +149,7 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
- 注文タイプごとの価格整合条件とブローカー最小距離制約を満たす。
- `use_m15_entry_filter = true` の場合、T2/T4はM15確定足の方向・反発・接近条件を満たす。T1/T3は `use_m15_imbalance_confirmation = true` の場合にM15初動確認を満たす。
- 自EAの注文 + ポジション数が `input_position_limit` 未満。
- 分割エントリー有効時は `target_zones.txt``res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + slot の重複注文/ポジション不存在を満たす。
- 分割エントリー有効時は `target_zones.txt``res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + 注文タイプ + slot の重複注文/ポジション不存在を満たす。
### market_state と注文タイプ
@@ -160,6 +161,8 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
- `5 MARKET_HIGH_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。
- `6 MARKET_TECHNICAL_ERROR_STOP`: 新規注文を停止。
H4 `market_state` の再読込後は、更新後の状態で許可されない注文タイプの既存pending注文を取消する。`6 MARKET_TECHNICAL_ERROR_STOP` の場合は対象EAのpending注文をすべて取消する。
### 注文タイプごとの価格整合条件
- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry`
@@ -169,7 +172,7 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
## 7. エグジット条件
- 未約定注文は、注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。
- 未約定注文は、コメントからH1候補時刻を復元できる場合はその時刻から、復元できない旧注文は注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。
- 保有ポジションは、建玉時刻から `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合にクローズする。
- 時間制限処理は `OnTick` の早い段階で実行し、Python完了待ちやスプレッド判定に依存させない。
- EA削除時は `OnDeinit()` で外部プロセス状態とSLTPパネルを解放し、`Comment("")``ChartRedraw(0)` によりチャート上のステータス表示を消去する。
@@ -191,10 +194,13 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
- `input_position_limit` は口座全体ではなく、自EA対象分のみを数える。内部の絶対上限は `POSITION_LIMIT` とする。
- 送信時は `SYMBOL_FILLING_MODE` を参照し、IOC、FOK、RETURNの順で利用可能な執行ポリシーを選択する。
- 通常注文と分割注文の送信前に、ロットが `SYMBOL_VOLUME_MIN/MAX/STEP` を満たすことを検証する。
- ペンディング注文には、ブローカーが対応する場合にサーバー側の期限を設定する。
- ペンディング注文には、ブローカーが対応する場合にH1候補時刻を起点とするサーバー側の期限を設定する。発注時点で期限切れなら送信しない。
- `OrderSend` の戻り値と `MqlTradeResult.retcode` を確認し、失敗時は `GetLastError()` またはretcodeをログへ出力する。
- 分割エントリーの注文コメントには `candidate_id` とslot番号を入れ、同一slotの二重発注を抑止する。
- 通常注文と分割注文コメントには有効な `candidate_id` を入れ、候補時刻起点の期限管理に使用する。
- 分割エントリーの注文コメントには注文タイプとslot番号も入れ、同一戦略slotの二重発注を抑止する。
- `cancel_old_split_pending_on_new_zone = true` の場合、新しい有効 `candidate_id` を読んだ時は旧candidateの分割pending注文を取消し、`target_zones.txt` が無効または停止値の場合は既存の分割pending注文をすべて取消する。
- H4状態更新後は、更新後の `market_state` と矛盾する既存pending注文を取消する。
- pending取消に失敗したticketは `input_cancel_retry_cooldown_seconds` の間再送せず、毎tickの過剰な取引サーバー要求を防止する。
- SLTP管理は `_Symbol``magic_number` が一致する既存ポジションのみを対象とし、TPは既存値を維持する。
- SL変更は既存SLより利益保護方向へ改善する場合だけ実行し、ブローカーのstop level / freeze levelを満たさない候補は送信しない。
- `HighVolatilityLimit()` はBUYをBid、SELLをAsk基準で評価し、GOLD/XAUUSDはサフィックス付きシンボルでも1 pip = 0.1として扱う。
@@ -203,13 +209,27 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
- `trend_state.txt` が存在しない、読み込めない、整数として厳格にパースできない、または `0..6` 以外の場合は `MARKET_TECHNICAL_ERROR_STOP` として扱う。
- `target_prices.txt` が存在しない、読み込めない、13行未満、またはいずれかの行が数値として厳格にパースできない場合は `res_chk = 0` として扱う。
- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。
- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`有効ゾーンの `candidate_id` が厳格な12桁時刻ではない、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。
- Python側がH1候補生成を安全側停止値へ倒した場合は、`debug_entry.txt``RESULT: SAFE_STOP`、停止段階、理由、H4状態、候補IDを追記する。これにより通常のGPT見送りとAPI・設定・データ異常を区別できる。
- Pythonプロセスが異常終了した場合、終了コードをログへ出力し、次回トリガーで再実行できる状態に戻す。
- Pythonプロセスが `PYTHON_TIMEOUT_SECONDS` を超えても実行中の場合、`taskkill /T /F` でcmd/bat配下のプロセスツリーを終了し、done/runningファイルを整理して次回トリガーで再実行できる状態に戻す。
- `running` ファイルだけが残っている場合は、PIDからプロセス復元を試みる。復元できずタイムアウト済みなら古いマーカーとして削除する。
## 10. 変更履歴
### 2026-06-01
- H1候補生成が安全側停止値へフォールバックした場合も `debug_entry.txt` へ停止段階と理由を追記し、通常の候補見送りと障害停止を判別できるようにした。
### 2026-05-31
- 起動時は `initial_order` の値にかかわらずH4/H1結果を再生成し、H4再読込までは技術エラー停止値を維持するfail-closed初期化へ変更した。
- H4 `market_state` 更新後に、更新状態と矛盾する既存pending注文を取消する仕様を追加した。
- pending取消失敗時にticket単位のクールダウンを設け、毎tickの取消再送による取引サーバーへの過剰リクエストを抑止した。
- 分割slot識別子へ注文タイプを追加し、同一 `candidate_id` の異なる戦略がslotを共有しても衝突しないようにした。
- 通常注文と分割注文のコメントへ有効な `candidate_id` を保持し、pending期限をH1候補時刻起点で管理するようにした。
- 有効な `target_zones.txt``candidate_id` は厳格な12桁数字時刻を要求し、不正値は安全側で停止するようにした。
### 2026-05-30
- 同一Terminal内の複数EA/複数シンボルでPython連携ファイルが衝突しないよう、`HIT_<symbol>_<magic_number>` 接頭辞をMT5/Python双方のファイル名へ適用する仕様に更新した。
@@ -0,0 +1,259 @@
# HIT-EA 仕様書(日本語)
## 1. 概要
- 本EAは、H4の確定足OHLCから外部Pythonで相場状態を判定し、H1の確定足OHLCからエントリー候補価格を生成する。
- MQL5側はPythonの出力ファイルを読み込み、H4相場状態、H1候補価格、M15確定足フィルタ、スプレッド、ブローカー距離制約を確認したうえでペンディング注文を発行する。
- バックテスト再現性のため、Pythonへ渡すOHLCは `OHLC_START_SHIFT = 1` により形成中バーを除外し、確定足だけを使用する。
- ライブ実行時の耐性として、外部プロセスの `running` ファイル、プロセスID、終了コード、タイムアウトを監視し、二重起動とCSV上書きを抑止する。
## 2. 使用インジケータ
- 標準インジケータおよびカスタムインジケータは未使用。
- OHLCは `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)` で取得する。
- M15エントリー確認は、T2/T4では直近確定足と1本前の確定足のローソク足形状、平均レンジ、候補価格との距離から判定し、T1/T3では必要に応じてM15初動確認を行う。
## 3. パラメータ設定
### input
- `lot_size = 0.01`: 注文ロット。
- `spread_limit = 60`: 許容スプレッド(point)。
- `magic_number = 10001`: EA識別用マジックナンバー。
- `initial_order = 0`: 起動直後の初回H4/H1処理を実行するか。`1` で有効。
- `use_m15_entry_filter = true`: M15確定足によるエントリータイミング確認を使用するか。
- `m15_entry_zone_atr_multiplier = 1.50`: M15平均レンジに対する候補価格接近許容倍率。
- `use_m15_imbalance_confirmation = true`: T1/T3の順張り候補に対して、M15確定足の初動確認を追加するか。
- `m15_imbalance_avg_body_period = 20`: M15初動確認で使う平均実体計算期間。
- `m15_imbalance_sensitivity = 2.0`: M15初動確認で現在足実体が平均実体を上回る必要倍率。
- `m15_imbalance_min_avg_body_points = 1.0`: M15平均実体の最小値(point)。
- `use_m15_imbalance_debug_log = false`: M15初動確認の詳細ログを出すか。
- `input_entry_max_candidate_age_minutes = 120`: H1候補価格を新規発注に使用できる最大経過分数。古い候補でM15条件が後から整った場合の遅延エントリーを抑止する。
- `input_position_limit = 10`: 自EAの未約定注文 + ポジション数の実運用上限。内部上限 `POSITION_LIMIT` を超える指定は丸める。
- `use_split_entry_zone = false`: H1予測ゾーンを使った分割エントリーを有効化するか。
- `split_entry_count = 3`: 分割エントリー本数。1〜10に丸めて使用する。
- `split_lot_mode = SPLIT_LOT_TOTAL`: 分割ロット配分モード。`SPLIT_LOT_TOTAL` は総ロットをN分割、`SPLIT_LOT_FIXED` は各注文に固定ロットを使う。
- `split_total_lot_size = 0.09`: 総量分割モードの合計ロット。
- `split_fixed_lot_size = 0.01`: 固定ロットモードの1注文ロット。
- `cancel_old_split_pending_on_new_zone = true`: 新しいH1ゾーン読込時に、同じEAの古い分割pending注文を取消するか。
- `input_sltp_manager_enabled = false`: UI連動の `CSLTPManager` による既存ポジションSL管理を有効化するか。
- `input_sltp_show_panel = true`: チャート上にSLTP操作パネルを表示するか。
- `input_sltp_use_breakeven = false`: 通常ブレークイーブンを有効化するか。
- `input_sltp_breakeven_trigger_pips = 30.0`: 通常ブレークイーブンを開始する含み益pips。
- `input_sltp_breakeven_buffer_pips = 3.0`: 通常ブレークイーブン時に建値から利益方向へずらすpips。
- `input_sltp_use_elapsed_breakeven = false`: 保有時間ベースのブレークイーブンを有効化するか。
- `input_sltp_elapsed_breakeven_hours = 4.0`: 保有時間ベースのブレークイーブンを許可するまでの保有時間。
- `input_sltp_elapsed_breakeven_buffer_pips = 3.0`: 保有時間ベースのブレークイーブン時に建値から利益方向へずらすpips。
- `input_sltp_use_active_trailing = false`: アクティブトレーリングを有効化するか。
- `input_sltp_active_breakeven_pips = 30.0`: アクティブトレーリング開始pips。
- `input_sltp_active_stop_loss_offset_pips = 5.0`: 開始時に建値から固定する利益pips。
- `input_sltp_active_step_trigger_pips = 10.0`: SLを1段進めるために必要な追加利益pips。
- `input_sltp_active_step_move_pips = 5.0`: 1段ごとにSLを利益方向へ動かすpips。
- `input_sltp_use_tp_progress_stop = false`: TP進捗率に応じたSL固定を有効化するか。
- `input_sltp_tp_progress_trigger_percent = 70.0`: TP進捗率SLを開始する進捗率。
- `input_sltp_tp_progress_sl_lock_percent = 30.0`: 建値からTPまでの距離のうちSLで固定する割合。
- `input_sltp_use_high_volatility_limit = false`: 短時間急変時のSL引き締めを有効化するか。
### 主要定数
- `POSITION_LIMIT = 48`: 自EAの未約定注文 + ポジション数の上限。
- `ENTRY_H1_LIMIT = 2`: 未約定注文の有効期限(H1本数)。
- `CLOSE_H1_LIMIT = 12`: ポジションの時間制限クローズ(H1本数)。
- `HISTORY_BARS = 72`: Pythonへ渡すOHLC本数。
- `OHLC_START_SHIFT = 1`: 確定足からOHLCを取得するためのシフト。
- `M15_CONFIRM_BARS = 30`: M15平均レンジ計算に使う足数。
- `ENTRY_RETRY_SECONDS = 60`: エントリー判定リトライ間隔。
- `ENTRY_RETRY_LIMIT = 10`: H1候補価格に対する最大リトライ回数。
- `TARGET_SIZE = 13`: `target_prices.txt` から読み込む数値数。
- `TARGET_ZONE_SCHEMA_VERSION = 2`: `target_zones.txt` の形式バージョン。
- `PYTHON_TIMEOUT_SECONDS = 600`: Python完了待ちの監視上限。
### Python側エントリー候補ガード
- H1候補価格生成後、Python側で現在価格から遠すぎる `entry` を無効化する。
- 距離上限は戦略別に計算し、T1/T3のStop系は `max(H1 EATR * 1.50, 5.00)`、T2/T4のLimit系は `max(H1 EATR * 1.00, 5.00)` とする。上限を超えた戦略は `0.00,0.00,0.00` に補正する。
- このガードは、深い指値や遠いブレイク待ちがM15確認後に遅れて発注されることを抑えるための後処理である。
## 4. ファイル構成
- `Experts/MyProject/HIT-EA_refactor_ver6.mq5`: EAエントリーポイント。`OnInit`, `OnDeinit`, `OnTick`、input、グローバル状態、DLL importを保持する。
- `Include/MyLib/Common/HITRuntimeController.mqh`: ティック処理、H4/H1/M15更新制御、ステータス表示、スプレッド判定。
- `Include/MyLib/Signals/HITEntrySignal.mqh`: エントリー前提条件、注文タイプ別価格判定、M15フィルタ、リトライ状態更新。
- `Include/MyLib/Common/HITExternalProcess.mqh`: done/runningファイル、外部プロセスハンドル、PID復元、終了コード確認、タイムアウト復旧。
- `Include/MyLib/Signals/HITPythonSignalGateway.mqh`: OHLC CSV出力、Pythonバッチ起動、`trend_state.txt` / `target_prices.txt` / `target_zones.txt` 読込。
- `Include/MyLib/Trading/HITTradeManager.mqh`: 注文送信、執行ポリシー、有効期限設定、注文/ポジション数集計、期限切れ取消/クローズ。
- `Include/MyLib/Trading/SLTPManager.mqh`: 既存ポジションに対するブレークイーブン、時間経過BE、アクティブトレーリング、TP進捗SL、急変時SL引き締めを管理する。
- `Include/MyLib/Panel/SLTPManagerPanel.mqh`: `CSLTPManager` の各設定をチャート上で切り替え、数値入力を検証してEAへ反映する操作パネル。
## 5. Python連携ファイル
EAは `OnInit()` で `_Symbol` と `magic_number` から `HIT_<sanitized symbol>_<magic_number>` 形式の接頭辞を作り、MT5 `Files` 配下の連携ファイル名へ付与する。例: `HIT_GOLD_10001_ohlc_H4.csv`。batには同じ接頭辞を第1引数で渡し、bat側が `MT5_EA_FILE_PREFIX` としてPythonへ引き継ぐ。第2引数には `_Symbol` の `SYMBOL_DIGITS` を渡し、bat側が `MT5_PRICE_DIGITS` としてPythonへ引き継ぐ。
手動実行などで接頭辞が指定されない場合、Pythonは従来どおり `ohlc_H4.csv` などの旧ファイル名を使う。
### H4トレンド判定
- MQL5出力: `<prefix>_ohlc_H4.csv`
- Python出力: `<prefix>_trend_state.txt`
- 完了フラグ: `<prefix>_process_done_trend.txt`
- 実行中フラグ: `<prefix>_process_running_trend.txt`
- 起動バッチ: `<TerminalDataPath>\MQL5\python_for_ea\bat\get_trend_reply.bat`
### H1エントリー価格生成
- MQL5出力: `<prefix>_ohlc_H1.csv`
- Python出力: `<prefix>_target_prices.txt`, `<prefix>_target_zones.txt`
- 完了フラグ: `<prefix>_process_done_entry.txt`
- 実行中フラグ: `<prefix>_process_running_entry.txt`
- 起動バッチ: `<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行構成とする。
```text
1行目: res_chk
2-4行目: T1 Buy Stop の entry/tp/sl
5-7行目: T2 Buy Limit の entry/tp/sl
8-10行目: T3 Sell Stop の entry/tp/sl
11-13行目: T4 Sell Limit の entry/tp/sl
```
`target_zones.txt` は分割エントリー用の7行構成とする。
```text
1行目: schema_version2
2行目: res_chk
3行目: candidate_idH1確定足時刻由来)
4行目: T1 Buy Stop の strategy, zone_low, zone_high, tp, sl
5行目: T2 Buy Limit の strategy, zone_low, zone_high, tp, sl
6行目: T3 Sell Stop の strategy, zone_low, zone_high, tp, sl
7行目: T4 Sell Limit の strategy, zone_low, zone_high, tp, sl
```
Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えてから `process_done_entry.txt` を作成する。`target_zones.txt` の価格小数桁は `MT5_PRICE_DIGITS` に合わせ、未指定時は安全側で5桁を使う。分割エントリーが無効な場合、EAは従来どおり `target_prices.txt` を使用する。
## 6. エントリー条件
新規エントリーは、以下をすべて満たす場合のみ行う。
- スプレッドが `spread_limit` 以下。
- H4トレンド判定結果が完了済みで、`trend_state.txt` を読み込める。
- H1候補価格生成が完了済みで、`target_prices.txt` を読み込める。
- `res_chk = 1`。
- H1候補価格が、元になったH1確定足時刻から `ENTRY_H1_LIMIT` 内で期限切れではない。
- H1候補価格が、元になったH1確定足時刻から `input_entry_max_candidate_age_minutes` 分を超えていない。
- 対象注文タイプの `entry/tp/sl` がすべて `0.0` より大きい。
- H4 `market_state` に対して注文タイプが許可されている。
- 注文タイプごとの価格整合条件とブローカー最小距離制約を満たす。
- `use_m15_entry_filter = true` の場合、T2/T4はM15確定足の方向・反発・接近条件を満たす。T1/T3は `use_m15_imbalance_confirmation = true` の場合にM15初動確認を満たす。
- 自EAの注文 + ポジション数が `input_position_limit` 未満。
- 分割エントリー有効時は `target_zones.txt` の `res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + slot の重複注文/ポジション不存在を満たす。
### market_state と注文タイプ
- `0 MARKET_LOW_VOL_RANGE`: T2 Buy Limit / T4 Sell Limit を許可。
- `1 MARKET_HIGH_VOL_RANGE`: T2 Buy Limit / T4 Sell Limit を許可。
- `2 MARKET_LOW_VOL_UP`: T1 Buy Stop / T2 Buy Limit を許可。
- `3 MARKET_HIGH_VOL_UP`: T1 Buy Stop / T2 Buy Limit を許可。
- `4 MARKET_LOW_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。
- `5 MARKET_HIGH_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。
- `6 MARKET_TECHNICAL_ERROR_STOP`: 新規注文を停止。
### 注文タイプごとの価格整合条件
- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry`
- T2 Buy Limit: `Ask > entry`, `tp > entry`, `sl < entry`
- T3 Sell Stop: `Bid > entry`, `tp < entry`, `sl > entry`
- T4 Sell Limit: `Bid < entry`, `tp < entry`, `sl > entry`
## 7. エグジット条件
- 未約定注文は、注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。
- 保有ポジションは、建玉時刻から `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合にクローズする。
- 時間制限処理は `OnTick` の早い段階で実行し、Python完了待ちやスプレッド判定に依存させない。
- EA削除時は `OnDeinit()` で外部プロセス状態とSLTPパネルを解放し、`Comment("")` と `ChartRedraw(0)` によりチャート上のステータス表示を消去する。
- `input_sltp_manager_enabled` またはパネルの `Manager` がONの場合、時間制限処理後に `CSLTPManager::ManagePositions()` と `HighVolatilityLimit()` を実行する。これにより、Python結果待ちや新規注文停止中でも既存ポジションのSL保護を継続する。
- 通常ブレークイーブンとアクティブトレーリングは同時有効化しない。パネル上では片方をONにするともう片方をOFFにし、`CSLTPManager::ValidateSettings()` でも排他条件を検証する。
## 7.1 SLTP操作パネル
- パネルは `input_sltp_show_panel = true` の場合に作成され、チャート右上に表示される。
- パネル生成に失敗した場合はログへ出力し、EA本体はinputで反映済みのSLTP設定のまま継続する。
- `Manager` ボタンでSLTP管理全体のON/OFFを切り替える。OFFの場合、パネル値は保持するが `OnTick` でSL変更は行わない。
- `BreakEven`, `Elapsed BE`, `Active Trail`, `TP Progress`, `High Vol` ボタンで各機能を切り替える。
- 数値入力欄は `APPLY` 押下時に範囲検証される。範囲外または数値以外の場合は `MessageBox` とログで通知し、直前の有効値へ戻す。
- `APPLY` 成功時のみEA内部の `CSLTPManager` 設定へ反映する。反映後は `ValidateSettings()` を通し、失敗した場合はSLTP管理を停止してログへ理由を出力する。
## 8. リスク管理
- 注文/ポジション管理対象は `_Symbol` と `magic_number` が一致するものに限定する。
- `input_position_limit` は口座全体ではなく、自EA対象分のみを数える。内部の絶対上限は `POSITION_LIMIT` とする。
- 送信時は `SYMBOL_FILLING_MODE` を参照し、IOC、FOK、RETURNの順で利用可能な執行ポリシーを選択する。
- 通常注文と分割注文の送信前に、ロットが `SYMBOL_VOLUME_MIN/MAX/STEP` を満たすことを検証する。
- ペンディング注文には、ブローカーが対応する場合にサーバー側の期限を設定する。
- `OrderSend` の戻り値と `MqlTradeResult.retcode` を確認し、失敗時は `GetLastError()` またはretcodeをログへ出力する。
- 分割エントリーの注文コメントには `candidate_id` とslot番号を入れ、同一slotの二重発注を抑止する。
- `cancel_old_split_pending_on_new_zone = true` の場合、新しい有効 `candidate_id` を読んだ時は旧candidateの分割pending注文を取消し、`target_zones.txt` が無効または停止値の場合は既存の分割pending注文をすべて取消する。
- SLTP管理は `_Symbol` と `magic_number` が一致する既存ポジションのみを対象とし、TPは既存値を維持する。
- SL変更は既存SLより利益保護方向へ改善する場合だけ実行し、ブローカーのstop level / freeze levelを満たさない候補は送信しない。
- `HighVolatilityLimit()` はBUYをBid、SELLをAsk基準で評価し、GOLD/XAUUSDはサフィックス付きシンボルでも1 pip = 0.1として扱う。
## 9. 異常系の扱い
- `trend_state.txt` が存在しない、読み込めない、整数として厳格にパースできない、または `0..6` 以外の場合は `MARKET_TECHNICAL_ERROR_STOP` として扱う。
- `target_prices.txt` が存在しない、読み込めない、13行未満、またはいずれかの行が数値として厳格にパースできない場合は `res_chk = 0` として扱う。
- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。
- Pythonプロセスが異常終了した場合、終了コードをログへ出力し、次回トリガーで再実行できる状態に戻す。
- Pythonプロセスが `PYTHON_TIMEOUT_SECONDS` を超えても実行中の場合、`taskkill /T /F` でcmd/bat配下のプロセスツリーを終了し、done/runningファイルを整理して次回トリガーで再実行できる状態に戻す。
- `running` ファイルだけが残っている場合は、PIDからプロセス復元を試みる。復元できずタイムアウト済みなら古いマーカーとして削除する。
## 10. 変更履歴
### 2026-05-30
- 同一Terminal内の複数EA/複数シンボルでPython連携ファイルが衝突しないよう、`HIT_<symbol>_<magic_number>` 接頭辞をMT5/Python双方のファイル名へ適用する仕様に更新した。
- T1/T3の順張り候補でも `use_m15_imbalance_confirmation = true` の場合はM15初動確認を必須とする仕様に更新した。
- `trend_state.txt`、`target_prices.txt`、`target_zones.txt` の数値読込を厳格化し、不正値は安全側の停止値として扱う仕様を追加した。
- `target_zones.txt` が無効または停止値の場合に、古い分割pending注文を全取消できる仕様を追加した。
- 現行コードに合わせて、`input_entry_max_candidate_age_minutes`、`ENTRY_H1_LIMIT`、SLTPバッファ、Python側距離ガードの既定値を修正した。
- Pythonタイムアウト時に実行中プロセスツリーを強制終了して再試行できる仕様を追加した。
- H1候補の鮮度判定をPython結果の読込時刻ではなく、`candidate_id` から復元したH1確定足時刻基準へ変更した。
- `MT5_PRICE_DIGITS` によりPython側の `target_zones.txt` 出力桁数をシンボル桁数へ合わせる仕様を追加した。
- 通常注文ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 検証、OHLC CSVのNaN/inf・OHLC整合性検証、SELL高ボラSLのAsk基準評価、GOLD/XAUUSDサフィックス対応を追加した。
### 2026-05-27
- Python補助アプリの配置を `C:\ea_py` からMT5データフォルダ配下の `MQL5\python_for_ea` へ移行する仕様に変更した。
- EA側は `TerminalInfoString(TERMINAL_DATA_PATH)` からbatパスを組み立て、外部プロセスの作業ディレクトリはbatファイルの配置から導出するようにした。
- Python側は `MQL5\python_for_ea` の親にある `MQL5\Files` を既定の連携先として自動検出し、必要に応じて `MT5_FILES_DIR` または `MT5_DATA_PATH` で上書きできる仕様にした。
### 2026-05-24
- Python側で `target_zones.txt` を追加出力し、既存 `target_prices.txt` と両方を書き終えてから `process_done_entry.txt` を作成する仕様にした。
- EA側に `use_split_entry_zone`、`split_entry_count`、`split_lot_mode`、`split_total_lot_size`、`split_fixed_lot_size`、`input_position_limit` を追加した。
- 分割エントリー有効時は、H1予測ゾーンをN本に分割し、総ロット分割または固定ロットでpending注文を出せるようにした。
- `candidate_id` + slot番号で分割注文を識別し、同一slotの二重発注を抑止する仕様を追加した。
### 2026-05-13
- Python側にH1 EATRベースの候補距離ガードを追加し、現在価格から遠すぎる候補をMT5へ渡す前に無効化する仕様を追加。
- EA側に `input_entry_max_candidate_age_minutes` を追加し、M15確認が遅れて整った古いH1候補で新規発注しないようにした。
- エントリー見送りログへH1候補の経過秒数を追加し、遅延・M15未確認・価格不整合の原因を追跡しやすくした。
### 2026-05-05
- EA削除後にチャート左上のステータスコメントが残らないよう、`OnDeinit()` で `Comment("")` と `ChartRedraw(0)` を実行する終了処理を追加。
- `HIT-EA_refactor_ver5.mq5` に `CSLTPManager` と専用チャートパネル `SLTPManagerPanel.mqh` を接続し、UIからブレークイーブン、時間経過BE、アクティブトレーリング、TP進捗SL、急変時SL引き締めを操作できるようにした。
- SLTP管理を `OnTick` の時間制限処理直後に実行し、Python結果待ちやスプレッド判定に依存せず既存ポジション保護を継続する仕様を追加。
- SLTPパネル用inputと、通常ブレークイーブン/アクティブトレーリングの排他検証、`APPLY` 時の数値検証を追加。
- `HIT-EA_refactor_ver5.mq5` の関数群を用途別 `.mqh` に分割。
- `OnInit`, `OnDeinit`, `OnTick`、input、グローバル状態はEA本体に残し、動作ロジックは `Include/MyLib/` 配下へ移動。
- 現行コードに合わせて、`CreateProcessW` ベースのPythonプロセス監視、M15フィルタ、`market_state` 0..6、`spread_limit = 60` を仕様へ反映。
### 2026-05-02
- `res_chk=0` 時にエントリー送信を抑止する分岐を追加。
- 時間制限処理を、Python完了待ち・スプレッド制限より前に移動。
- `initial_order` の初回処理フラグをH4用・H1用に分離。
- OHLC取得を `CopyRates` 中心に変更し、確定足のみをPythonへ渡す仕様に変更。
@@ -0,0 +1,274 @@
# HIT-EA 仕様書(日本語)
## 1. 概要
- 本EAは、H4の確定足OHLCから外部Pythonで相場状態を判定し、H1の確定足OHLCからエントリー候補価格を生成する。
- MQL5側はPythonの出力ファイルを読み込み、H4相場状態、H1候補価格、M15確定足フィルタ、スプレッド、ブローカー距離制約を確認したうえでペンディング注文を発行する。
- バックテスト再現性のため、Pythonへ渡すOHLCは `OHLC_START_SHIFT = 1` により形成中バーを除外し、確定足だけを使用する。
- ライブ実行時の耐性として、外部プロセスの `running` ファイル、プロセスID、終了コード、タイムアウトを監視し、二重起動とCSV上書きを抑止する。
## 2. 使用インジケータ
- 標準インジケータおよびカスタムインジケータは未使用。
- OHLCは `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)` で取得する。
- M15エントリー確認は、T2/T4では直近確定足と1本前の確定足のローソク足形状、平均レンジ、候補価格との距離から判定し、T1/T3では必要に応じてM15初動確認を行う。
## 3. パラメータ設定
### input
- `lot_size = 0.01`: 注文ロット。
- `spread_limit = 60`: 許容スプレッド(point)。
- `magic_number = 10001`: EA識別用マジックナンバー。
- `initial_order = 0`: 旧設定ファイルとの互換性維持用。値にかかわらず起動時はH4/H1を再生成し、H4再読込までは技術エラー停止状態を維持する。
- `use_m15_entry_filter = true`: M15確定足によるエントリータイミング確認を使用するか。
- `m15_entry_zone_atr_multiplier = 1.50`: M15平均レンジに対する候補価格接近許容倍率。
- `use_m15_imbalance_confirmation = true`: T1/T3の順張り候補に対して、M15確定足の初動確認を追加するか。
- `m15_imbalance_avg_body_period = 20`: M15初動確認で使う平均実体計算期間。
- `m15_imbalance_sensitivity = 2.0`: M15初動確認で現在足実体が平均実体を上回る必要倍率。
- `m15_imbalance_min_avg_body_points = 1.0`: M15平均実体の最小値(point)。
- `use_m15_imbalance_debug_log = false`: M15初動確認の詳細ログを出すか。
- `input_entry_max_candidate_age_minutes = 120`: H1候補価格を新規発注に使用できる最大経過分数。古い候補でM15条件が後から整った場合の遅延エントリーを抑止する。
- `input_position_limit = 10`: 自EAの未約定注文 + ポジション数の実運用上限。内部上限 `POSITION_LIMIT` を超える指定は丸める。
- `use_split_entry_zone = false`: H1予測ゾーンを使った分割エントリーを有効化するか。
- `split_entry_count = 3`: 分割エントリー本数。1〜10に丸めて使用する。
- `split_lot_mode = SPLIT_LOT_TOTAL`: 分割ロット配分モード。`SPLIT_LOT_TOTAL` は総ロットをN分割、`SPLIT_LOT_FIXED` は各注文に固定ロットを使う。
- `split_total_lot_size = 0.09`: 総量分割モードの合計ロット。
- `split_fixed_lot_size = 0.01`: 固定ロットモードの1注文ロット。
- `cancel_old_split_pending_on_new_zone = true`: 新しいH1ゾーン読込時に、同じEAの古い分割pending注文を取消するか。
- `input_cancel_retry_cooldown_seconds = 60`: pending注文の取消に失敗した場合、同一ticketへ取消要求を再送するまでの待機秒数。1〜3600秒に丸めて使用する。
- `input_sltp_manager_enabled = false`: UI連動の `CSLTPManager` による既存ポジションSL管理を有効化するか。
- `input_sltp_show_panel = true`: チャート上にSLTP操作パネルを表示するか。
- `input_sltp_use_breakeven = false`: 通常ブレークイーブンを有効化するか。
- `input_sltp_breakeven_trigger_pips = 30.0`: 通常ブレークイーブンを開始する含み益pips。
- `input_sltp_breakeven_buffer_pips = 3.0`: 通常ブレークイーブン時に建値から利益方向へずらすpips。
- `input_sltp_use_elapsed_breakeven = false`: 保有時間ベースのブレークイーブンを有効化するか。
- `input_sltp_elapsed_breakeven_hours = 4.0`: 保有時間ベースのブレークイーブンを許可するまでの保有時間。
- `input_sltp_elapsed_breakeven_buffer_pips = 3.0`: 保有時間ベースのブレークイーブン時に建値から利益方向へずらすpips。
- `input_sltp_use_active_trailing = false`: アクティブトレーリングを有効化するか。
- `input_sltp_active_breakeven_pips = 30.0`: アクティブトレーリング開始pips。
- `input_sltp_active_stop_loss_offset_pips = 5.0`: 開始時に建値から固定する利益pips。
- `input_sltp_active_step_trigger_pips = 10.0`: SLを1段進めるために必要な追加利益pips。
- `input_sltp_active_step_move_pips = 5.0`: 1段ごとにSLを利益方向へ動かすpips。
- `input_sltp_use_tp_progress_stop = false`: TP進捗率に応じたSL固定を有効化するか。
- `input_sltp_tp_progress_trigger_percent = 70.0`: TP進捗率SLを開始する進捗率。
- `input_sltp_tp_progress_sl_lock_percent = 30.0`: 建値からTPまでの距離のうちSLで固定する割合。
- `input_sltp_use_high_volatility_limit = false`: 短時間急変時のSL引き締めを有効化するか。
### 主要定数
- `POSITION_LIMIT = 48`: 自EAの未約定注文 + ポジション数の上限。
- `ENTRY_H1_LIMIT = 2`: 未約定注文の有効期限(H1本数)。
- `CLOSE_H1_LIMIT = 12`: ポジションの時間制限クローズ(H1本数)。
- `HISTORY_BARS = 72`: Pythonへ渡すOHLC本数。
- `OHLC_START_SHIFT = 1`: 確定足からOHLCを取得するためのシフト。
- `M15_CONFIRM_BARS = 30`: M15平均レンジ計算に使う足数。
- `ENTRY_RETRY_SECONDS = 60`: エントリー判定リトライ間隔。
- `ENTRY_RETRY_LIMIT = 10`: H1候補価格に対する最大リトライ回数。
- `TARGET_SIZE = 13`: `target_prices.txt` から読み込む数値数。
- `TARGET_ZONE_SCHEMA_VERSION = 2`: `target_zones.txt` の形式バージョン。
- `PYTHON_TIMEOUT_SECONDS = 600`: Python完了待ちの監視上限。
### Python側エントリー候補ガード
- H1候補価格生成後、Python側で現在価格から遠すぎる `entry` を無効化する。
- 距離上限は戦略別に計算し、T1/T3のStop系は `max(H1 EATR * 1.50, 5.00)`、T2/T4のLimit系は `max(H1 EATR * 1.00, 5.00)` とする。上限を超えた戦略は `0.00,0.00,0.00` に補正する。
- このガードは、深い指値や遠いブレイク待ちがM15確認後に遅れて発注されることを抑えるための後処理である。
## 4. ファイル構成
- `Experts/MyProject/HIT-EA_refactor_ver6.mq5`: EAエントリーポイント。`OnInit`, `OnDeinit`, `OnTick`、input、グローバル状態、DLL importを保持する。
- `Include/MyLib/Common/HITRuntimeController.mqh`: ティック処理、H4/H1/M15更新制御、ステータス表示、スプレッド判定。
- `Include/MyLib/Signals/HITEntrySignal.mqh`: エントリー前提条件、注文タイプ別価格判定、M15フィルタ、リトライ状態更新。
- `Include/MyLib/Common/HITExternalProcess.mqh`: done/runningファイル、外部プロセスハンドル、PID復元、終了コード確認、タイムアウト復旧。
- `Include/MyLib/Signals/HITPythonSignalGateway.mqh`: OHLC CSV出力、Pythonバッチ起動、`trend_state.txt` / `target_prices.txt` / `target_zones.txt` 読込。
- `Include/MyLib/Trading/HITTradeManager.mqh`: 注文送信、執行ポリシー、有効期限設定、注文/ポジション数集計、期限切れ取消/クローズ。
- `Include/MyLib/Trading/SLTPManager.mqh`: 既存ポジションに対するブレークイーブン、時間経過BE、アクティブトレーリング、TP進捗SL、急変時SL引き締めを管理する。
- `Include/MyLib/Panel/SLTPManagerPanel.mqh`: `CSLTPManager` の各設定をチャート上で切り替え、数値入力を検証してEAへ反映する操作パネル。
## 5. Python連携ファイル
EAは `OnInit()` で `_Symbol` と `magic_number` から `HIT_<sanitized symbol>_<magic_number>` 形式の接頭辞を作り、MT5 `Files` 配下の連携ファイル名へ付与する。例: `HIT_GOLD_10001_ohlc_H4.csv`。batには同じ接頭辞を第1引数で渡し、bat側が `MT5_EA_FILE_PREFIX` としてPythonへ引き継ぐ。第2引数には `_Symbol` の `SYMBOL_DIGITS` を渡し、bat側が `MT5_PRICE_DIGITS` としてPythonへ引き継ぐ。
手動実行などで接頭辞が指定されない場合、Pythonは従来どおり `ohlc_H4.csv` などの旧ファイル名を使う。
### H4トレンド判定
- MQL5出力: `<prefix>_ohlc_H4.csv`
- Python出力: `<prefix>_trend_state.txt`
- 完了フラグ: `<prefix>_process_done_trend.txt`
- 実行中フラグ: `<prefix>_process_running_trend.txt`
- 起動バッチ: `<TerminalDataPath>\MQL5\python_for_ea\bat\get_trend_reply.bat`
### H1エントリー価格生成
- MQL5出力: `<prefix>_ohlc_H1.csv`
- Python出力: `<prefix>_target_prices.txt`, `<prefix>_target_zones.txt`
- 完了フラグ: `<prefix>_process_done_entry.txt`
- 実行中フラグ: `<prefix>_process_running_entry.txt`
- 起動バッチ: `<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行構成とする。
```text
1行目: res_chk
2-4行目: T1 Buy Stop の entry/tp/sl
5-7行目: T2 Buy Limit の entry/tp/sl
8-10行目: T3 Sell Stop の entry/tp/sl
11-13行目: T4 Sell Limit の entry/tp/sl
```
`target_zones.txt` は分割エントリー用の7行構成とする。
```text
1行目: schema_version2
2行目: res_chk
3行目: candidate_idH1確定足時刻由来)
4行目: T1 Buy Stop の strategy, zone_low, zone_high, tp, sl
5行目: T2 Buy Limit の strategy, zone_low, zone_high, tp, sl
6行目: T3 Sell Stop の strategy, zone_low, zone_high, tp, sl
7行目: T4 Sell Limit の strategy, zone_low, zone_high, tp, sl
```
Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えてから `process_done_entry.txt` を作成する。`target_zones.txt` の価格小数桁は `MT5_PRICE_DIGITS` に合わせ、未指定時は安全側で5桁を使う。分割エントリーが無効な場合、EAは従来どおり `target_prices.txt` を使用する。
## 6. エントリー条件
新規エントリーは、以下をすべて満たす場合のみ行う。
- スプレッドが `spread_limit` 以下。
- H4トレンド判定結果が完了済みで、`trend_state.txt` を読み込める。
- H1候補価格生成が完了済みで、`target_prices.txt` を読み込める。
- `res_chk = 1`。
- H1候補価格が、元になったH1確定足時刻から `ENTRY_H1_LIMIT` 内で期限切れではない。
- H1候補価格が、元になったH1確定足時刻から `input_entry_max_candidate_age_minutes` 分を超えていない。
- 対象注文タイプの `entry/tp/sl` がすべて `0.0` より大きい。
- H4 `market_state` に対して注文タイプが許可されている。
- 注文タイプごとの価格整合条件とブローカー最小距離制約を満たす。
- `use_m15_entry_filter = true` の場合、T2/T4はM15確定足の方向・反発・接近条件を満たす。T1/T3は `use_m15_imbalance_confirmation = true` の場合にM15初動確認を満たす。
- 自EAの注文 + ポジション数が `input_position_limit` 未満。
- 分割エントリー有効時は `target_zones.txt` の `res_chk = 1`、予測ゾーンの価格整合、分割ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 適合、同一 `candidate_id` + 注文タイプ + slot の重複注文/ポジション不存在を満たす。
### market_state と注文タイプ
- `0 MARKET_LOW_VOL_RANGE`: T2 Buy Limit / T4 Sell Limit を許可。
- `1 MARKET_HIGH_VOL_RANGE`: T2 Buy Limit / T4 Sell Limit を許可。
- `2 MARKET_LOW_VOL_UP`: T1 Buy Stop / T2 Buy Limit を許可。
- `3 MARKET_HIGH_VOL_UP`: T1 Buy Stop / T2 Buy Limit を許可。
- `4 MARKET_LOW_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。
- `5 MARKET_HIGH_VOL_DOWN`: T3 Sell Stop / T4 Sell Limit を許可。
- `6 MARKET_TECHNICAL_ERROR_STOP`: 新規注文を停止。
H4 `market_state` の再読込後は、更新後の状態で許可されない注文タイプの既存pending注文を取消する。`6 MARKET_TECHNICAL_ERROR_STOP` の場合は対象EAのpending注文をすべて取消する。
### 注文タイプごとの価格整合条件
- T1 Buy Stop: `Ask < entry`, `tp > entry`, `sl < entry`
- T2 Buy Limit: `Ask > entry`, `tp > entry`, `sl < entry`
- T3 Sell Stop: `Bid > entry`, `tp < entry`, `sl > entry`
- T4 Sell Limit: `Bid < entry`, `tp < entry`, `sl > entry`
## 7. エグジット条件
- 未約定注文は、コメントからH1候補時刻を復元できる場合はその時刻から、復元できない旧注文は注文作成時刻から `ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合に取消する。
- 保有ポジションは、建玉時刻から `CLOSE_H1_LIMIT * PeriodSeconds(PERIOD_H1)` 以上経過した場合にクローズする。
- 時間制限処理は `OnTick` の早い段階で実行し、Python完了待ちやスプレッド判定に依存させない。
- EA削除時は `OnDeinit()` で外部プロセス状態とSLTPパネルを解放し、`Comment("")` と `ChartRedraw(0)` によりチャート上のステータス表示を消去する。
- `input_sltp_manager_enabled` またはパネルの `Manager` がONの場合、時間制限処理後に `CSLTPManager::ManagePositions()` と `HighVolatilityLimit()` を実行する。これにより、Python結果待ちや新規注文停止中でも既存ポジションのSL保護を継続する。
- 通常ブレークイーブンとアクティブトレーリングは同時有効化しない。パネル上では片方をONにするともう片方をOFFにし、`CSLTPManager::ValidateSettings()` でも排他条件を検証する。
## 7.1 SLTP操作パネル
- パネルは `input_sltp_show_panel = true` の場合に作成され、チャート右上に表示される。
- パネル生成に失敗した場合はログへ出力し、EA本体はinputで反映済みのSLTP設定のまま継続する。
- `Manager` ボタンでSLTP管理全体のON/OFFを切り替える。OFFの場合、パネル値は保持するが `OnTick` でSL変更は行わない。
- `BreakEven`, `Elapsed BE`, `Active Trail`, `TP Progress`, `High Vol` ボタンで各機能を切り替える。
- 数値入力欄は `APPLY` 押下時に範囲検証される。範囲外または数値以外の場合は `MessageBox` とログで通知し、直前の有効値へ戻す。
- `APPLY` 成功時のみEA内部の `CSLTPManager` 設定へ反映する。反映後は `ValidateSettings()` を通し、失敗した場合はSLTP管理を停止してログへ理由を出力する。
## 8. リスク管理
- 注文/ポジション管理対象は `_Symbol` と `magic_number` が一致するものに限定する。
- `input_position_limit` は口座全体ではなく、自EA対象分のみを数える。内部の絶対上限は `POSITION_LIMIT` とする。
- 送信時は `SYMBOL_FILLING_MODE` を参照し、IOC、FOK、RETURNの順で利用可能な執行ポリシーを選択する。
- 通常注文と分割注文の送信前に、ロットが `SYMBOL_VOLUME_MIN/MAX/STEP` を満たすことを検証する。
- ペンディング注文には、ブローカーが対応する場合にH1候補時刻を起点とするサーバー側の期限を設定する。発注時点で期限切れなら送信しない。
- `OrderSend` の戻り値と `MqlTradeResult.retcode` を確認し、失敗時は `GetLastError()` またはretcodeをログへ出力する。
- 通常注文と分割注文のコメントには有効な `candidate_id` を入れ、候補時刻起点の期限管理に使用する。
- 分割エントリーの注文コメントには注文タイプとslot番号も入れ、同一戦略slotの二重発注を抑止する。
- `cancel_old_split_pending_on_new_zone = true` の場合、新しい有効 `candidate_id` を読んだ時は旧candidateの分割pending注文を取消し、`target_zones.txt` が無効または停止値の場合は既存の分割pending注文をすべて取消する。
- H4状態更新後は、更新後の `market_state` と矛盾する既存pending注文を取消する。
- pending取消に失敗したticketは `input_cancel_retry_cooldown_seconds` の間再送せず、毎tickの過剰な取引サーバー要求を防止する。
- SLTP管理は `_Symbol` と `magic_number` が一致する既存ポジションのみを対象とし、TPは既存値を維持する。
- SL変更は既存SLより利益保護方向へ改善する場合だけ実行し、ブローカーのstop level / freeze levelを満たさない候補は送信しない。
- `HighVolatilityLimit()` はBUYをBid、SELLをAsk基準で評価し、GOLD/XAUUSDはサフィックス付きシンボルでも1 pip = 0.1として扱う。
## 9. 異常系の扱い
- `trend_state.txt` が存在しない、読み込めない、整数として厳格にパースできない、または `0..6` 以外の場合は `MARKET_TECHNICAL_ERROR_STOP` として扱う。
- `target_prices.txt` が存在しない、読み込めない、13行未満、またはいずれかの行が数値として厳格にパースできない場合は `res_chk = 0` として扱う。
- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、`res_chk != 1`、有効ゾーンの `candidate_id` が厳格な12桁時刻ではない、またはいずれかの数値行を厳格にパースできない場合は新規注文を停止する。
- Pythonプロセスが異常終了した場合、終了コードをログへ出力し、次回トリガーで再実行できる状態に戻す。
- Pythonプロセスが `PYTHON_TIMEOUT_SECONDS` を超えても実行中の場合、`taskkill /T /F` でcmd/bat配下のプロセスツリーを終了し、done/runningファイルを整理して次回トリガーで再実行できる状態に戻す。
- `running` ファイルだけが残っている場合は、PIDからプロセス復元を試みる。復元できずタイムアウト済みなら古いマーカーとして削除する。
## 10. 変更履歴
### 2026-05-31
- 起動時は `initial_order` の値にかかわらずH4/H1結果を再生成し、H4再読込までは技術エラー停止値を維持するfail-closed初期化へ変更した。
- H4 `market_state` 更新後に、更新状態と矛盾する既存pending注文を取消する仕様を追加した。
- pending取消失敗時にticket単位のクールダウンを設け、毎tickの取消再送による取引サーバーへの過剰リクエストを抑止した。
- 分割slot識別子へ注文タイプを追加し、同一 `candidate_id` の異なる戦略がslotを共有しても衝突しないようにした。
- 通常注文と分割注文のコメントへ有効な `candidate_id` を保持し、pending期限をH1候補時刻起点で管理するようにした。
- 有効な `target_zones.txt` の `candidate_id` は厳格な12桁数字時刻を要求し、不正値は安全側で停止するようにした。
### 2026-05-30
- 同一Terminal内の複数EA/複数シンボルでPython連携ファイルが衝突しないよう、`HIT_<symbol>_<magic_number>` 接頭辞をMT5/Python双方のファイル名へ適用する仕様に更新した。
- T1/T3の順張り候補でも `use_m15_imbalance_confirmation = true` の場合はM15初動確認を必須とする仕様に更新した。
- `trend_state.txt`、`target_prices.txt`、`target_zones.txt` の数値読込を厳格化し、不正値は安全側の停止値として扱う仕様を追加した。
- `target_zones.txt` が無効または停止値の場合に、古い分割pending注文を全取消できる仕様を追加した。
- 現行コードに合わせて、`input_entry_max_candidate_age_minutes`、`ENTRY_H1_LIMIT`、SLTPバッファ、Python側距離ガードの既定値を修正した。
- Pythonタイムアウト時に実行中プロセスツリーを強制終了して再試行できる仕様を追加した。
- H1候補の鮮度判定をPython結果の読込時刻ではなく、`candidate_id` から復元したH1確定足時刻基準へ変更した。
- `MT5_PRICE_DIGITS` によりPython側の `target_zones.txt` 出力桁数をシンボル桁数へ合わせる仕様を追加した。
- 通常注文ロットの `SYMBOL_VOLUME_MIN/MAX/STEP` 検証、OHLC CSVのNaN/inf・OHLC整合性検証、SELL高ボラSLのAsk基準評価、GOLD/XAUUSDサフィックス対応を追加した。
### 2026-05-27
- Python補助アプリの配置を `C:\ea_py` からMT5データフォルダ配下の `MQL5\python_for_ea` へ移行する仕様に変更した。
- EA側は `TerminalInfoString(TERMINAL_DATA_PATH)` からbatパスを組み立て、外部プロセスの作業ディレクトリはbatファイルの配置から導出するようにした。
- Python側は `MQL5\python_for_ea` の親にある `MQL5\Files` を既定の連携先として自動検出し、必要に応じて `MT5_FILES_DIR` または `MT5_DATA_PATH` で上書きできる仕様にした。
### 2026-05-24
- Python側で `target_zones.txt` を追加出力し、既存 `target_prices.txt` と両方を書き終えてから `process_done_entry.txt` を作成する仕様にした。
- EA側に `use_split_entry_zone`、`split_entry_count`、`split_lot_mode`、`split_total_lot_size`、`split_fixed_lot_size`、`input_position_limit` を追加した。
- 分割エントリー有効時は、H1予測ゾーンをN本に分割し、総ロット分割または固定ロットでpending注文を出せるようにした。
- `candidate_id` + slot番号で分割注文を識別し、同一slotの二重発注を抑止する仕様を追加した。
### 2026-05-13
- Python側にH1 EATRベースの候補距離ガードを追加し、現在価格から遠すぎる候補をMT5へ渡す前に無効化する仕様を追加。
- EA側に `input_entry_max_candidate_age_minutes` を追加し、M15確認が遅れて整った古いH1候補で新規発注しないようにした。
- エントリー見送りログへH1候補の経過秒数を追加し、遅延・M15未確認・価格不整合の原因を追跡しやすくした。
### 2026-05-05
- EA削除後にチャート左上のステータスコメントが残らないよう、`OnDeinit()` で `Comment("")` と `ChartRedraw(0)` を実行する終了処理を追加。
- `HIT-EA_refactor_ver5.mq5` に `CSLTPManager` と専用チャートパネル `SLTPManagerPanel.mqh` を接続し、UIからブレークイーブン、時間経過BE、アクティブトレーリング、TP進捗SL、急変時SL引き締めを操作できるようにした。
- SLTP管理を `OnTick` の時間制限処理直後に実行し、Python結果待ちやスプレッド判定に依存せず既存ポジション保護を継続する仕様を追加。
- SLTPパネル用inputと、通常ブレークイーブン/アクティブトレーリングの排他検証、`APPLY` 時の数値検証を追加。
- `HIT-EA_refactor_ver5.mq5` の関数群を用途別 `.mqh` に分割。
- `OnInit`, `OnDeinit`, `OnTick`、input、グローバル状態はEA本体に残し、動作ロジックは `Include/MyLib/` 配下へ移動。
- 現行コードに合わせて、`CreateProcessW` ベースのPythonプロセス監視、M15フィルタ、`market_state` 0..6、`spread_limit = 60` を仕様へ反映。
### 2026-05-02
- `res_chk=0` 時にエントリー送信を抑止する分岐を追加。
- 時間制限処理を、Python完了待ち・スプレッド制限より前に移動。
- `initial_order` の初回処理フラグをH4用・H1用に分離。
- OHLC取得を `CopyRates` 中心に変更し、確定足のみをPythonへ渡す仕様に変更。
+5 -1
View File
@@ -92,7 +92,8 @@ MT5 が読む結果ファイルは `utf-16 LE` で出力します。結果ファ
3. H1 の短期 36 本、中期 72 本からチャート PNG と数値要約を作る。
4. H1 インバランス初動を Python 側で判定し、矛盾する候補を抑止する。
5. OpenAI Responses API に候補価格・予測ゾーン生成を依頼する。
6. GPT 出力を既存13行形式と分割エントリー用ゾーン形式へ展開し、価格の大小関係と距離を検証する。
出力は Structured Outputs のJSON Schemaで固定する。
6. GPT JSON出力を既存13行形式と分割エントリー用ゾーン形式へ展開し、価格の大小関係、距離、reward/riskを検証する。
7. `target_prices.txt``target_zones.txt` を書き、両方の完了後に `process_done_entry.txt` を作成する。
H4 状態ごとの許可戦略:
@@ -132,6 +133,9 @@ EA側で分割エントリーを有効にした場合は、この予測ゾーン
## OpenAI 設定
H1 エントリー候補生成では OpenAI API を使用します。
Responses APIの `text.format` にJSON Schemaを渡し、GPT出力を自然文やCSV風テキストではなく
`schema_version``strategies` を持つ構造化JSONに固定します。
JSONが不正、API応答が未完了、価格条件やreward/risk条件に合わない場合は停止値へ倒します。
必須:
@@ -207,8 +207,8 @@ EAファイルはこのPythonプロジェクトの外部連携先として扱う
| `src/ea_py/charting/candlestick.py` | ローソク足PNG生成。 |
| `src/ea_py/market/volatility.py` | True Range、Exponential ATR、ボラティリティ分類。 |
| `src/ea_py/market/trend_state.py` | H4方向判定結果とボラティリティ分類の合成。 |
| `src/ea_py/market/target_prices.py` | GPT出力のパース、価格整合性チェック、13行形式への変換。 |
| `src/ea_py/market/target_zones.py` | GPT出力予測ゾーン分割エントリー用7行形式へ変換。 |
| `src/ea_py/market/target_prices.py` | 既存13行形式への変換と価格整合性チェック。 |
| `src/ea_py/market/target_zones.py` | Structured OutputsのGPT JSON出力予測ゾーンへ変換し、分割エントリー用7行形式へ展開。 |
| `src/ea_py/market/imbalance.py` | H1インバランス初動判定。 |
| `src/ea_py/prompts/trend_prompt.py` | H4相場環境判定プロンプトの生成。 |
| `src/ea_py/prompts/entry_prompt.py` | H1候補価格生成プロンプトの生成。 |
+2
View File
@@ -8,6 +8,7 @@ DEFAULT_GPT_MODEL = "gpt-5.5-2026-04-23"
DEFAULT_REASONING_EFFORT = "low"
VALID_REASONING_EFFORTS = frozenset({"none", "low", "medium", "high", "xhigh"})
DEBUG_PRINT = True
ENTRY_RESPONSE_SCHEMA_VERSION = 1
ATR_PERIOD = 14
ATR_BASELINE_PERIOD = 20
@@ -24,6 +25,7 @@ IMBALANCE_MIN_AVG_BODY_SIZE = 0.01
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER = 1.00
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER = 1.50
ENTRY_MAX_DISTANCE_MIN_PRICE = 5.0
MIN_ENTRY_REWARD_RISK_RATIO = 1.20
CANDLE_TREND = 72
CANDLE_SHORT = 36
+28
View File
@@ -103,3 +103,31 @@ def append_debug_entry(
file.write("---- REASON START ----\n")
file.write((reason_text or "").strip() + "\n")
file.write("---- REASON END ----\n\n")
def append_debug_entry_stop(
*,
path: Path,
stage: str,
reason: str,
trend_state: int,
selected_strategies: Sequence[int] = (),
candidate_id: str = "",
current_price: float | None = None,
imbalance_summary: str = "",
) -> None:
"""H1候補生成が安全側停止値へ倒れた理由を追記する。"""
with path.open("a", encoding="utf-8") as file:
file.write("=" * 60 + "\n")
file.write(f"DEBUG TIME : {now_str()}\n")
file.write("RESULT : SAFE_STOP\n")
file.write(f"STAGE : {stage}\n")
file.write(f"REASON : {reason}\n")
file.write(f"MARKET_STATE(H4) : {trend_state} ({MARKET_STATE_LABELS.get(trend_state, 'UNKNOWN')})\n")
file.write(f"SELECTED_STRATEGY : {','.join(str(x) for x in selected_strategies) or '-'}\n")
file.write(f"CANDIDATE_ID : {candidate_id or '-'}\n")
if current_price is not None:
file.write(f"CURRENT PRICE : {current_price:.2f}\n")
if imbalance_summary:
file.write(f"IMBALANCE(H1) : {imbalance_summary}\n")
file.write("=" * 60 + "\n\n")
@@ -0,0 +1,105 @@
"""デバッグ理由ログをMT5 Files配下へ追記する。"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import Sequence
from ea_py.constants import MARKET_STATE_LABELS
def now_str() -> str:
"""デバッグログ用の現在時刻文字列を返す。"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def append_debug_trend(
*,
path: Path,
model: str,
reasoning_effort: str,
max_output_tokens: int,
api_diagnostics: str,
current_price: float,
numeric_summary: str,
direction_numeric: str,
market_state: int,
classification_reason: str,
reason_text: str,
) -> None:
"""H4トレンド判定の数値要約、方向出力、分類理由を追記する。"""
with path.open("a", encoding="utf-8") as file:
file.write("=" * 60 + "\n")
file.write(f"DEBUG TIME : {now_str()}\n")
file.write(f"MODEL : {model}\n")
file.write(f"REASONING : {reasoning_effort}\n")
file.write(f"MAX TOKENS : {max_output_tokens}\n")
file.write(f"CURRENT PRICE : {current_price:.2f}\n")
file.write(f"MARKET STATE : {market_state} ({MARKET_STATE_LABELS.get(market_state, 'UNKNOWN')})\n")
file.write("=" * 60 + "\n\n")
file.write("---- API DIAGNOSTICS START ----\n")
file.write((api_diagnostics or "").strip() + "\n")
file.write("---- API DIAGNOSTICS END ----\n\n")
file.write("---- NUMERIC SUMMARY START ----\n")
file.write((numeric_summary or "").strip() + "\n")
file.write("---- NUMERIC SUMMARY END ----\n\n")
file.write("---- DIRECTION NUMERIC (0/1/2) START ----\n")
file.write((direction_numeric or "").strip() + "\n")
file.write("---- DIRECTION NUMERIC (0/1/2) END ----\n\n")
file.write("---- MARKET STATE CLASSIFICATION START ----\n")
file.write((classification_reason or "").strip() + "\n")
file.write("---- MARKET STATE CLASSIFICATION END ----\n\n")
file.write("---- REASON START ----\n")
file.write((reason_text or "").strip() + "\n")
file.write("---- REASON END ----\n\n")
def append_debug_entry(
*,
path: Path,
model: str,
reasoning_effort: str,
max_output_tokens: int,
api_diagnostics: str,
timeframe: str,
current_price: float,
trend_state: int,
selected_strategies: Sequence[int],
imbalance_summary: str,
numeric_summary: str,
numeric_lines: str,
post_filter_summary: str,
sanitized_numeric_list: Sequence[int | float] | None,
reason_text: str,
) -> None:
"""H1エントリー候補生成の数値行と理由を追記する。"""
with path.open("a", encoding="utf-8") as file:
file.write("=" * 60 + "\n")
file.write(f"DEBUG TIME : {now_str()}\n")
file.write(f"MODEL : {model}\n")
file.write(f"REASONING : {reasoning_effort}\n")
file.write(f"MAX TOKENS : {max_output_tokens}\n")
file.write(f"TIMEFRAME : {timeframe}\n")
file.write(f"MARKET_STATE(H4) : {trend_state} ({MARKET_STATE_LABELS.get(trend_state, 'UNKNOWN')})\n")
file.write(f"SELECTED_STRATEGY : {','.join(str(x) for x in selected_strategies)}\n")
file.write(f"IMBALANCE(H1) : {imbalance_summary}\n")
file.write(f"CURRENT PRICE : {current_price:.2f}\n")
file.write("=" * 60 + "\n\n")
file.write("---- API DIAGNOSTICS START ----\n")
file.write((api_diagnostics or "").strip() + "\n")
file.write("---- API DIAGNOSTICS END ----\n\n")
file.write("---- NUMERIC SUMMARY START ----\n")
file.write((numeric_summary or "").strip() + "\n")
file.write("---- NUMERIC SUMMARY END ----\n\n")
file.write("---- GPT NUMERIC LINES START ----\n")
file.write((numeric_lines or "").strip() + "\n")
file.write("---- GPT NUMERIC LINES END ----\n\n")
file.write("---- POST FILTER START ----\n")
file.write((post_filter_summary or "").strip() + "\n")
if sanitized_numeric_list is not None:
file.write("sanitized_numeric_list=" + ",".join(str(x) for x in sanitized_numeric_list) + "\n")
file.write("---- POST FILTER END ----\n\n")
file.write("---- REASON START ----\n")
file.write((reason_text or "").strip() + "\n")
file.write("---- REASON END ----\n\n")
+12 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
import json
import logging
@@ -30,6 +30,10 @@ class ResponsesApiDiagnostics:
error: str
usage: str
def is_completed(self) -> bool:
"""ライブ判定に使える完了レスポンスかどうかを返す。"""
return self.status == "completed" and not self.incomplete_details and not self.error
def to_log_text(self) -> str:
"""デバッグログへそのまま書ける1行診断文字列を返す。"""
parts = [
@@ -89,6 +93,7 @@ def call_responses_api(
user_text: str,
image_data_urls: Sequence[str],
max_output_tokens: int,
response_text_format: Mapping[str, Any] | None = None,
) -> ResponsesApiResult:
"""Responses APIへテキストとチャート画像を送り、出力テキストを返す。
@@ -96,6 +101,9 @@ def call_responses_api(
`user_text` はH4/H1の具体的な分析依頼、数値要約、出力フォーマットを含む。
`image_data_urls` にはPNGをdata URL化したチャート画像を渡す。空文字は無視する。
`response_text_format` が指定された場合は Responses API の `text.format` へ渡し、
JSON Schemaなどの構造化出力をAPI側でも強制する。
戻り値は `response.output_text` をstripした文字列とAPI診断情報。
API例外や空/不正な出力の安全側処理は、この薄いラッパーではなく
呼び出し元のパイプラインとパース関数が担当する。
@@ -121,6 +129,9 @@ def call_responses_api(
"reasoning": {"effort": reasoning_effort},
"max_output_tokens": max_output_tokens,
}
if response_text_format is not None:
create_params["text"] = {"format": dict(response_text_format)}
response = client.responses.create(**create_params)
diagnostics = _extract_response_diagnostics(response)
text = (response.output_text or "").strip()
@@ -11,7 +11,6 @@ from ea_py.constants import (
CANDLE_LONG,
CANDLE_SHORT,
DEBUG_PRINT,
ENTRY_DEBUG_MAX_OUTPUT_TOKENS,
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER,
ENTRY_MAX_DISTANCE_MIN_PRICE,
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER,
@@ -22,23 +21,23 @@ from ea_py.constants import (
IMBALANCE_SENSITIVITY,
INSTRUMENT,
MARKET_STATE_LABELS,
MIN_ENTRY_REWARD_RISK_RATIO,
MT_ENCODING,
TECHNICAL_ERROR_STOP,
USE_IMBALANCE_FILTER,
)
from ea_py.io.debug_logs import append_debug_entry
from ea_py.io.debug_logs import append_debug_entry, append_debug_entry_stop
from ea_py.io.mt5_files import read_int_file, write_results_then_done
from ea_py.io.ohlc_csv import read_ohlc_csv
from ea_py.market.volatility import summarize_ohlc
from ea_py.market.target_prices import (
extract_entry_blocks_debug,
stop_numeric_list,
strategies_by_trend,
)
from ea_py.market.target_zones import (
build_candidate_id,
format_target_zones,
parse_lines_to_entry_zones_allow_subset,
parse_json_to_entry_zones,
sanitize_entry_zones,
stop_entry_zones,
zones_to_numeric_list,
@@ -49,12 +48,12 @@ from ea_py.market.imbalance import (
format_imbalance_summary,
)
from ea_py.openai_client import call_responses_api, create_openai_client
from ea_py.paths import build_entry_paths
from ea_py.paths import EntryPaths, build_entry_paths
from ea_py.prompts.entry_prompt import (
build_imbalance_guidance,
build_caution_block,
build_common_rules_block,
build_common_rules_block_debug,
build_entry_response_text_format,
build_header,
build_market_state_guidance,
build_numeric_summary,
@@ -68,6 +67,41 @@ logger = logging.getLogger(__name__)
TREND_STOP_STRATEGIES = frozenset({1, 3})
def exception_summary(error: Exception) -> str:
"""秘密情報を追加せず、例外型とメッセージを1行へ丸める。"""
return " ".join(f"{type(error).__name__}: {error}".splitlines()).strip()
def write_safe_stop(
*,
paths: EntryPaths,
stage: str,
reason: str,
trend_state: int,
selected_strategies: Sequence[int] = (),
candidate_id: str = "",
current_price: float | None = None,
imbalance_summary: str = "",
) -> None:
"""停止値を出力し、ライブ診断用の理由ログも可能な限り残す。"""
if DEBUG_PRINT:
try:
append_debug_entry_stop(
path=paths.debug_reason,
stage=stage,
reason=reason,
trend_state=trend_state,
selected_strategies=selected_strategies,
candidate_id=candidate_id,
current_price=current_price,
imbalance_summary=imbalance_summary,
)
except Exception:
logger.exception("debug_entry.txt safe-stop write error")
write_entry_output(stop_numeric_list())
def write_entry_output(numeric_list: NumericList, zone_text: str | None = None) -> None:
"""target_prices.txtとtarget_zones.txtを書き、完了後にprocess_done_entry.txtを作る。"""
paths = build_entry_paths()
@@ -125,19 +159,37 @@ def run_pipeline() -> None:
trend_state,
MARKET_STATE_LABELS.get(trend_state, "UNKNOWN"),
)
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="market_state",
reason="H4 market_state blocks new entries.",
trend_state=trend_state,
selected_strategies=selected_strategies,
)
return
try:
ohlc_all = read_ohlc_csv(paths.input_csv)
except Exception:
except Exception as error:
logger.exception("CSV 読み込みエラー")
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="ohlc_csv",
reason=exception_summary(error),
trend_state=trend_state,
selected_strategies=selected_strategies,
)
return
if len(ohlc_all) < CANDLE_LONG:
logger.error("データ本数不足: len(ohlc)=%s (need >= %s)", len(ohlc_all), CANDLE_LONG)
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="ohlc_bars",
reason=f"Insufficient H1 bars: actual={len(ohlc_all)} required={CANDLE_LONG}.",
trend_state=trend_state,
selected_strategies=selected_strategies,
)
return
ohlc_short = ohlc_all[-CANDLE_SHORT:]
@@ -165,7 +217,16 @@ def run_pipeline() -> None:
MARKET_STATE_LABELS.get(trend_state, "UNKNOWN"),
imbalance_summary,
)
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="h1_imbalance",
reason="H1 imbalance conflicts with the allowed H4 strategies.",
trend_state=trend_state,
selected_strategies=selected_strategies,
candidate_id=candidate_id,
current_price=current_price,
imbalance_summary=imbalance_summary,
)
return
try:
@@ -187,9 +248,18 @@ def run_pipeline() -> None:
png_file_to_data_url(paths.tmp_short_chart),
png_file_to_data_url(paths.tmp_long_chart),
]
except Exception:
except Exception as error:
logger.exception("画像生成エラー")
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="chart_render",
reason=exception_summary(error),
trend_state=trend_state,
selected_strategies=selected_strategies,
candidate_id=candidate_id,
current_price=current_price,
imbalance_summary=imbalance_summary,
)
return
numeric_summary = build_numeric_summary(
@@ -206,7 +276,8 @@ def run_pipeline() -> None:
f"h1_eatr={h1_eatr:.2f}, "
f"stop_multiplier={ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER:.2f}, "
f"limit_multiplier={ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER:.2f}, "
f"floor={ENTRY_MAX_DISTANCE_MIN_PRICE:.2f}"
f"floor={ENTRY_MAX_DISTANCE_MIN_PRICE:.2f}, "
f"min_reward_risk={MIN_ENTRY_REWARD_RISK_RATIO:.2f}"
)
header = build_header(
current_price=current_price,
@@ -219,19 +290,24 @@ def run_pipeline() -> None:
try:
config = load_runtime_config(debug_print=DEBUG_PRINT)
except RuntimeError:
except RuntimeError as error:
logger.exception("OpenAI 設定エラー")
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="openai_config",
reason=exception_summary(error),
trend_state=trend_state,
selected_strategies=selected_strategies,
candidate_id=candidate_id,
current_price=current_price,
imbalance_summary=imbalance_summary,
)
return
if config.debug_print:
system_content = build_system_content_block_debug()
common_rules = build_common_rules_block_debug(selected_strategies, max_entry_distance)
max_tokens = ENTRY_DEBUG_MAX_OUTPUT_TOKENS
else:
system_content = build_system_content_block()
common_rules = build_common_rules_block(selected_strategies, max_entry_distance)
max_tokens = ENTRY_MAX_OUTPUT_TOKENS
system_content = build_system_content_block_debug() if config.debug_print else build_system_content_block()
common_rules = build_common_rules_block(selected_strategies, max_entry_distance)
response_text_format = build_entry_response_text_format(selected_strategies)
max_tokens = ENTRY_MAX_OUTPUT_TOKENS
user_text = "\n\n".join([header, market_state_guidance, imbalance_guidance, common_rules, caution]).strip()
@@ -246,12 +322,35 @@ def run_pipeline() -> None:
user_text=user_text,
image_data_urls=images_data_urls,
max_output_tokens=max_tokens,
response_text_format=response_text_format,
)
gpt_reply = gpt_result.text
api_diagnostics = gpt_result.diagnostics.to_log_text()
except Exception:
if not gpt_result.diagnostics.is_completed():
logger.error("OpenAI response incomplete or failed: %s", api_diagnostics)
write_safe_stop(
paths=paths,
stage="openai_response",
reason=api_diagnostics,
trend_state=trend_state,
selected_strategies=selected_strategies,
candidate_id=candidate_id,
current_price=current_price,
imbalance_summary=imbalance_summary,
)
return
except Exception as error:
logger.exception("OpenAI APIエラー")
write_entry_output(stop_numeric_list())
write_safe_stop(
paths=paths,
stage="openai_api",
reason=exception_summary(error),
trend_state=trend_state,
selected_strategies=selected_strategies,
candidate_id=candidate_id,
current_price=current_price,
imbalance_summary=imbalance_summary,
)
return
if config.debug_print:
@@ -262,21 +361,18 @@ def run_pipeline() -> None:
gpt_reply,
)
entry_zones = parse_json_to_entry_zones(gpt_reply)
entry_zones = sanitize_entry_zones(
entry_zones,
selected_strategies,
current_price,
max_entry_distance=max_entry_distance,
min_reward_risk_ratio=MIN_ENTRY_REWARD_RISK_RATIO,
)
numeric_list = zones_to_numeric_list(entry_zones)
zone_text = format_target_zones(entry_zones, candidate_id)
if config.debug_print:
numeric_lines, reason_text = extract_entry_blocks_debug(gpt_reply)
if not numeric_lines:
numeric_lines = gpt_reply
entry_zones = parse_lines_to_entry_zones_allow_subset(numeric_lines)
entry_zones = sanitize_entry_zones(
entry_zones,
selected_strategies,
current_price,
max_entry_distance=max_entry_distance,
)
numeric_list = zones_to_numeric_list(entry_zones)
zone_text = format_target_zones(entry_zones, candidate_id)
try:
append_debug_entry(
path=paths.debug_reason,
@@ -290,23 +386,13 @@ def run_pipeline() -> None:
selected_strategies=selected_strategies,
imbalance_summary=imbalance_summary,
numeric_summary=numeric_summary,
numeric_lines=numeric_lines,
numeric_lines=gpt_reply,
post_filter_summary=post_filter_summary,
sanitized_numeric_list=numeric_list,
reason_text=f"{reason_text}\n\nTARGET_ZONES\n{zone_text}".strip(),
reason_text=f"STRUCTURED_JSON\n{gpt_reply}\n\nTARGET_ZONES\n{zone_text}".strip(),
)
except Exception:
logger.exception("debug_entry.txt write error")
else:
entry_zones = parse_lines_to_entry_zones_allow_subset(gpt_reply)
entry_zones = sanitize_entry_zones(
entry_zones,
selected_strategies,
current_price,
max_entry_distance=max_entry_distance,
)
numeric_list = zones_to_numeric_list(entry_zones)
zone_text = format_target_zones(entry_zones, candidate_id)
write_entry_output(numeric_list, zone_text)
@@ -0,0 +1,305 @@
"""H1 OHLCとH4 market_stateからtarget_pricesを生成するパイプライン。"""
from __future__ import annotations
import logging
from collections.abc import Sequence
from ea_py.charting.candlestick import ohlc_to_candlestick_png_file, png_file_to_data_url
from ea_py.config import load_runtime_config
from ea_py.constants import (
CANDLE_LONG,
CANDLE_SHORT,
DEBUG_PRINT,
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER,
ENTRY_MAX_DISTANCE_MIN_PRICE,
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER,
ENTRY_MAX_OUTPUT_TOKENS,
ENTRY_TIMEFRAME,
IMBALANCE_AVG_BODY_PERIOD,
IMBALANCE_MIN_AVG_BODY_SIZE,
IMBALANCE_SENSITIVITY,
INSTRUMENT,
MARKET_STATE_LABELS,
MIN_ENTRY_REWARD_RISK_RATIO,
MT_ENCODING,
TECHNICAL_ERROR_STOP,
USE_IMBALANCE_FILTER,
)
from ea_py.io.debug_logs import append_debug_entry
from ea_py.io.mt5_files import read_int_file, write_results_then_done
from ea_py.io.ohlc_csv import read_ohlc_csv
from ea_py.market.volatility import summarize_ohlc
from ea_py.market.target_prices import (
stop_numeric_list,
strategies_by_trend,
)
from ea_py.market.target_zones import (
build_candidate_id,
format_target_zones,
parse_json_to_entry_zones,
sanitize_entry_zones,
stop_entry_zones,
zones_to_numeric_list,
)
from ea_py.market.imbalance import (
adjust_strategies_for_imbalance,
detect_imbalance_signal,
format_imbalance_summary,
)
from ea_py.openai_client import call_responses_api, create_openai_client
from ea_py.paths import build_entry_paths
from ea_py.prompts.entry_prompt import (
build_imbalance_guidance,
build_caution_block,
build_common_rules_block,
build_entry_response_text_format,
build_header,
build_market_state_guidance,
build_numeric_summary,
build_system_content_block,
build_system_content_block_debug,
)
from ea_py.types import NumericList
logger = logging.getLogger(__name__)
TREND_STOP_STRATEGIES = frozenset({1, 3})
def write_entry_output(numeric_list: NumericList, zone_text: str | None = None) -> None:
"""target_prices.txtとtarget_zones.txtを書き、完了後にprocess_done_entry.txtを作る。"""
paths = build_entry_paths()
content = "".join(f"{number}\n" for number in numeric_list)
zone_content = zone_text if zone_text is not None else format_target_zones(stop_entry_zones(), "0")
write_results_then_done(
result_files=[
(paths.output_prices, content, MT_ENCODING),
(paths.output_zones, zone_content, MT_ENCODING),
],
done_path=paths.done_entry,
)
def build_entry_distance_limits(h1_eatr: float, selected_strategies: Sequence[int]) -> dict[int, float]:
"""H1 EATRから戦略別のエントリー許容距離を作る。"""
limits: dict[int, float] = {}
for strategy in selected_strategies:
multiplier = (
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER
if strategy in TREND_STOP_STRATEGIES
else ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER
)
limits[strategy] = max(h1_eatr * multiplier, ENTRY_MAX_DISTANCE_MIN_PRICE)
return limits
def run_pipeline() -> None:
"""H1 OHLCとH4 market_stateからMT5向けtarget_pricesを生成する。
実行契約:
- `trend_state.txt` のH4 `market_state` を読み、許可戦略だけを選ぶ。
- `ohlc_H1.csv` から短期・中期チャート画像と数値要約を作る。
- OpenAIには選択済み戦略の候補価格だけを依頼し、返却行を13値形式へ展開する。
- GPT出力は `sanitize_numeric_list` で検証し、`target_prices.txt` を書いた後に
`process_done_entry.txt` を作成する。
安全側フォールバック:
H4が技術エラー停止値、CSV読込失敗、H1データ不足、画像生成失敗、OpenAI設定/API失敗、
GPT出力不正、または全候補が価格条件違反の場合は13値すべてを停止値にする。
Pythonは候補価格を作るだけで、注文送信やポジション操作は行わない。
"""
paths = build_entry_paths()
trend_state = read_int_file(
paths.trend_state,
encoding=MT_ENCODING,
allowed_values=MARKET_STATE_LABELS.keys(),
default=TECHNICAL_ERROR_STOP,
)
selected_strategies = strategies_by_trend(trend_state)
if trend_state == TECHNICAL_ERROR_STOP or not selected_strategies:
logger.info(
"market_state=%s (%s). New entries stopped.",
trend_state,
MARKET_STATE_LABELS.get(trend_state, "UNKNOWN"),
)
write_entry_output(stop_numeric_list())
return
try:
ohlc_all = read_ohlc_csv(paths.input_csv)
except Exception:
logger.exception("CSV 読み込みエラー")
write_entry_output(stop_numeric_list())
return
if len(ohlc_all) < CANDLE_LONG:
logger.error("データ本数不足: len(ohlc)=%s (need >= %s)", len(ohlc_all), CANDLE_LONG)
write_entry_output(stop_numeric_list())
return
ohlc_short = ohlc_all[-CANDLE_SHORT:]
ohlc_long = ohlc_all[-CANDLE_LONG:]
current_price = float(ohlc_short[-1]["Close"])
candidate_id = build_candidate_id(str(ohlc_short[-1].get("DateTime", "")))
imbalance_analysis = detect_imbalance_signal(
ohlc_all,
avg_body_period=IMBALANCE_AVG_BODY_PERIOD,
sensitivity=IMBALANCE_SENSITIVITY,
min_avg_body_size=IMBALANCE_MIN_AVG_BODY_SIZE,
)
imbalance_summary = format_imbalance_summary(imbalance_analysis)
selected_strategies = adjust_strategies_for_imbalance(
selected_strategies,
trend_state=trend_state,
analysis=imbalance_analysis,
use_filter=USE_IMBALANCE_FILTER,
)
if not selected_strategies:
logger.info(
"H1 imbalance conflicts with market_state=%s (%s). New entries stopped. %s",
trend_state,
MARKET_STATE_LABELS.get(trend_state, "UNKNOWN"),
imbalance_summary,
)
write_entry_output(stop_numeric_list())
return
try:
ohlc_to_candlestick_png_file(
ohlc_data=ohlc_short,
save_path=paths.tmp_short_chart,
instrument=INSTRUMENT,
timeframe=ENTRY_TIMEFRAME,
dark=True,
)
ohlc_to_candlestick_png_file(
ohlc_data=ohlc_long,
save_path=paths.tmp_long_chart,
instrument=INSTRUMENT,
timeframe=ENTRY_TIMEFRAME,
dark=True,
)
images_data_urls = [
png_file_to_data_url(paths.tmp_short_chart),
png_file_to_data_url(paths.tmp_long_chart),
]
except Exception:
logger.exception("画像生成エラー")
write_entry_output(stop_numeric_list())
return
numeric_summary = build_numeric_summary(
current_price=current_price,
ohlc_short=ohlc_short,
ohlc_long=ohlc_long,
)
short_summary = summarize_ohlc(ohlc_short)
h1_eatr = float(short_summary.get("eatr", 0.0))
max_entry_distance = build_entry_distance_limits(h1_eatr, selected_strategies)
post_filter_summary = (
"entry_distance_guard="
f"max_distance_by_strategy={max_entry_distance}, "
f"h1_eatr={h1_eatr:.2f}, "
f"stop_multiplier={ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER:.2f}, "
f"limit_multiplier={ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER:.2f}, "
f"floor={ENTRY_MAX_DISTANCE_MIN_PRICE:.2f}, "
f"min_reward_risk={MIN_ENTRY_REWARD_RISK_RATIO:.2f}"
)
header = build_header(
current_price=current_price,
numeric_summary=numeric_summary,
trend_state=trend_state,
)
market_state_guidance = build_market_state_guidance(trend_state)
imbalance_guidance = build_imbalance_guidance(imbalance_analysis)
caution = build_caution_block()
try:
config = load_runtime_config(debug_print=DEBUG_PRINT)
except RuntimeError:
logger.exception("OpenAI 設定エラー")
write_entry_output(stop_numeric_list())
return
system_content = build_system_content_block_debug() if config.debug_print else build_system_content_block()
common_rules = build_common_rules_block(selected_strategies, max_entry_distance)
response_text_format = build_entry_response_text_format(selected_strategies)
max_tokens = ENTRY_MAX_OUTPUT_TOKENS
user_text = "\n\n".join([header, market_state_guidance, imbalance_guidance, common_rules, caution]).strip()
api_diagnostics = ""
try:
client = create_openai_client(config.api_key)
gpt_result = call_responses_api(
client=client,
model=config.model,
reasoning_effort=config.reasoning_effort,
system_content=system_content,
user_text=user_text,
image_data_urls=images_data_urls,
max_output_tokens=max_tokens,
response_text_format=response_text_format,
)
gpt_reply = gpt_result.text
api_diagnostics = gpt_result.diagnostics.to_log_text()
if not gpt_result.diagnostics.is_completed():
logger.error("OpenAI response incomplete or failed: %s", api_diagnostics)
write_entry_output(stop_numeric_list())
return
except Exception:
logger.exception("OpenAI APIエラー")
write_entry_output(stop_numeric_list())
return
if config.debug_print:
logger.info(
"---- TREND ---- %s selected=%s\n---- GPT REPLY START ----\n%s\n---- GPT REPLY END ----",
trend_state,
selected_strategies,
gpt_reply,
)
entry_zones = parse_json_to_entry_zones(gpt_reply)
entry_zones = sanitize_entry_zones(
entry_zones,
selected_strategies,
current_price,
max_entry_distance=max_entry_distance,
min_reward_risk_ratio=MIN_ENTRY_REWARD_RISK_RATIO,
)
numeric_list = zones_to_numeric_list(entry_zones)
zone_text = format_target_zones(entry_zones, candidate_id)
if config.debug_print:
try:
append_debug_entry(
path=paths.debug_reason,
model=config.model,
reasoning_effort=config.reasoning_effort,
max_output_tokens=max_tokens,
api_diagnostics=api_diagnostics,
timeframe=ENTRY_TIMEFRAME,
current_price=current_price,
trend_state=trend_state,
selected_strategies=selected_strategies,
imbalance_summary=imbalance_summary,
numeric_summary=numeric_summary,
numeric_lines=gpt_reply,
post_filter_summary=post_filter_summary,
sanitized_numeric_list=numeric_list,
reason_text=f"STRUCTURED_JSON\n{gpt_reply}\n\nTARGET_ZONES\n{zone_text}".strip(),
)
except Exception:
logger.exception("debug_entry.txt write error")
write_entry_output(numeric_list, zone_text)
def main() -> None:
"""ログ設定を行ってH1エントリー候補生成パイプラインを起動する。"""
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s")
run_pipeline()
+111 -41
View File
@@ -3,33 +3,70 @@
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from ea_py.constants import CANDLE_LONG, CANDLE_SHORT, ENTRY_TIMEFRAME, MARKET_STATE_LABELS
from ea_py.constants import CANDLE_LONG, CANDLE_SHORT, ENTRY_RESPONSE_SCHEMA_VERSION, ENTRY_TIMEFRAME, MARKET_STATE_LABELS
from ea_py.market.imbalance import ImbalanceAnalysis, format_imbalance_summary
from ea_py.market.volatility import summarize_ohlc
from ea_py.types import OhlcBar
from ea_py.types import OhlcBar, OhlcSummary
ENTRY_REASON_CODES = (
"trend_breakout",
"pullback",
"range_edge",
"range_reversal",
"skip_weak_signal",
"skip_mid_range",
"skip_distance",
"skip_risk",
"skip_conflict",
"skip_other",
)
def build_system_content_block() -> str:
"""通常モード用のシステムメッセージを返す。"""
return (
"あなたは優秀な投資アドバイザーです。"
"ユーザーの指示を厳密に守り、指定された形式の数値のみを出力してください。"
"思考過程や説明文は一切出力してはいけません"
"あなたはアルゴリズム取引用の価格候補生成器です。"
"投資助言や自然文説明ではなく、指定JSON Schemaに一致する機械可読な候補だけを返してください。"
"ユーザーの制約、market_state、選択戦略、価格関係を厳密に守ってください"
"迷う場合は必ずskipにしてください。"
).strip()
def build_system_content_block_debug() -> str:
"""デバッグモード用のシステムメッセージを返す。"""
return (
"あなたは優秀な投資アドバイザーです。"
"ユーザーの指示を厳密に守ってください。"
"出力は必ず指定されたブロック構造に従ってください。"
"NUMERIC OUTPUT では指定フォーマットの数値行のみ。"
"REASON OUTPUT では理由を文章で簡潔に。"
"あなたはアルゴリズム取引用の価格候補生成器です。"
"指定JSON Schemaに一致する機械可読な候補だけを返してください。"
"自然文の理由や思考過程は出力せず、理由はreason_codeだけで表してください。"
).strip()
def _safe_ratio(numerator: float, denominator: float) -> float:
"""0除算を避けて比率を返す。"""
if denominator <= 0.0:
return 0.0
return numerator / denominator
def _format_position_metrics(summary: OhlcSummary, current_price: float) -> str:
"""レンジ内位置と高安までの距離を要約行へ追加する。"""
high = float(summary["high"])
low = float(summary["low"])
price_range = float(summary["range"])
eatr = float(summary["eatr"])
range_position = _safe_ratio(current_price - low, price_range)
distance_to_high = high - current_price
distance_to_low = current_price - low
return (
f" 現在価格レンジ位置={range_position:.2f}0=安値付近, 1=高値付近), "
f"高値まで={distance_to_high:.2f}({_safe_ratio(distance_to_high, eatr):.2f}EATR), "
f"安値まで={distance_to_low:.2f}({_safe_ratio(distance_to_low, eatr):.2f}EATR)"
)
def build_numeric_summary(
current_price: float,
ohlc_short: Sequence[OhlcBar],
@@ -50,6 +87,7 @@ def build_numeric_summary(
上ヒゲ={sum_short["latest_upper_wick"]:.2f}, 下ヒゲ={sum_short["latest_lower_wick"]:.2f},
平均実体={sum_short["avg_body"]:.2f},
上昇本数={sum_short["up"]}, 下落本数={sum_short["down"]}, 傾き={sum_short["slope"]:.4f}
{_format_position_metrics(sum_short, current_price)}
- 中期({sum_long["n"]}本):
高値={sum_long["high"]:.2f}, 安値={sum_long["low"]:.2f}, レンジ={sum_long["range"]:.2f},
@@ -58,6 +96,7 @@ def build_numeric_summary(
上ヒゲ={sum_long["latest_upper_wick"]:.2f}, 下ヒゲ={sum_long["latest_lower_wick"]:.2f},
平均実体={sum_long["avg_body"]:.2f},
上昇本数={sum_long["up"]}, 下落本数={sum_long["down"]}, 傾き={sum_long["slope"]:.4f}
{_format_position_metrics(sum_long, current_price)}
""".strip()
@@ -91,6 +130,8 @@ market_state = {trend_state}{MARKET_STATE_LABELS.get(trend_state, "UNKNOWN")}
【H1の役割】
- H1では、H4 market_state と整合する方向・セットアップだけを候補にしてください。
- H1がH4方向と明確に逆行、またはレンジ中央で優位性が弱い場合は見送ってください。
- 画像と数値要約が矛盾する場合は、バックテスト再現性を優先して数値要約を主根拠にしてください。
- 外部ニュース、経済指標、未知のファンダメンタル材料は考慮しないでください。
- 実際の発注タイミングはEA側のM15確定足フィルターで確認します。
H1ではM15の細かな反転を先読みせず、1時間以内に到達しうる妥当な候補価格を重視してください。
""".strip()
@@ -223,7 +264,7 @@ def build_common_rules_block(
"""選択戦略に応じた価格決定ルールとGPT出力形式を作る。
`selected_strategies` にはH4 market_stateと整合する戦略番号だけを渡す。
GPTには対象戦略のだけを、`戦略番号,entry,tp,sl,zone_low,zone_high` の数値行で返すよう指定する。
GPTには対象戦略の候補だけを、JSON Schemaに一致するオブジェクトで返すよう指定する。
各戦略のentry/tp/slの大小関係、1時間以内の到達条件、12時間以内の決済目線、
条件が弱い場合に `0.00,0.00,0.00,0.00,0.00` で見送るルールもここで明示する。
@@ -267,9 +308,10 @@ def build_common_rules_block(
- zone_high は予測ゾーンの高い価格
- エントリー基準価格は必ず zone_low 以上 zone_high 以下にしてください。
- 各価格の大小関係が、その戦略の条件と整合しているか必ず検証してください。
- それぞれの戦略において、利益の期待値が最大になるように価格を設定してください。
- reward/risk が 1.20 未満になる候補は、期待値が弱いため見送ってください。
- SL距離がH1の値幅に対して広すぎる、またはTPまでの余地が小さすぎる候補は見送ってください。
- 条件が弱い、ブレイク直後、レンジ中央付近、EATR基準でリスクが大きすぎる等で見送る場合は、
その戦略行を「戦略番号,0.00,0.00,0.00,0.00,0.00」としてください。
decision を "skip" とし、entry/tp/sl/zone_low/zone_high をすべて 0.0 にしてください。
{distance_rule}
【時間条件(全戦略共通)】
@@ -278,17 +320,13 @@ def build_common_rules_block(
- エントリー後12時間以内に利確・損切に到達しなければ、その時点の価格でクローズ。
【出力ルール(最重要)】
以下の形式で **対象戦略の行だけ** 出力してください。
- 1行につき1戦略
- 行の順序は {out_order}
- 各行は以下の6つをカンマ区切りで出力
戦略番号,エントリー基準価格,利確目標価格,ロスカット基準価格,予測ゾーン下限,予測ゾーン上限
- 数値のみを出力し、説明文・空行・記号は一切出力してはいけません。
- 各行の価格は必ず小数点以下2桁まで出力すること(例: 4812.62)。
- 見送り行も必ず小数点以下2桁の 0.00 を使うこと。
- Responses APIのJSON Schemaに一致するJSONオブジェクトだけを出力してください。
- schema_version は {ENTRY_RESPONSE_SCHEMA_VERSION}
- strategies には対象戦略だけを入れ、順序は {out_order}
- 各strategy要素は strategy, decision, entry, tp, sl, zone_low, zone_high, reason_code を必ず含めてください。
- decision は "use" または "skip" のみ。
- reason_code は次のいずれかのみ: {", ".join(ENTRY_REASON_CODES)}
- 自然文、Markdown、余分なキー、空行は一切出力しないでください。
""".strip()
@@ -296,23 +334,8 @@ def build_common_rules_block_debug(
selected_strategies: Sequence[int],
max_entry_distance: float | dict[int, float] | None = None,
) -> str:
"""理由出力を含む価格決定ルールと出力形式を作る"""
base = build_common_rules_block(selected_strategies, max_entry_distance)
tail = """
【デバッグ追加ルール】
デバッグモードのため、出力を次の3ブロック構成にしてください(順番固定)。
### NUMERIC OUTPUT ###
ここには、上記「出力ルール(最重要)」に従った“数値行のみ”をそのまま出力してください。
(余計な文字や空行は禁止)
### REASON OUTPUT ###
各出力行について、entry/tp/sl をそのように置いた意図を各1〜2行で説明してください。
最後に「market_state をどう解釈したか」を1〜2行でまとめてください。
### END ###
""".strip()
return f"{base}\n\n{tail}".strip()
"""デバッグ時もライブと同じStructured Outputsルールを使う"""
return build_common_rules_block(selected_strategies, max_entry_distance)
def build_caution_block() -> str:
@@ -321,3 +344,50 @@ def build_caution_block() -> str:
※ market_state とチャート/数値要約が矛盾すると判断した場合は、安全側に倒してください。
具体的には対象戦略を 0.00,0.00,0.00 で見送ってください。
""".strip()
def build_entry_response_text_format(selected_strategies: Sequence[int]) -> dict[str, Any]:
"""Responses API Structured Outputs用のJSON Schemaを返す。"""
strategy_enum = [int(strategy) for strategy in selected_strategies if strategy in (1, 2, 3, 4)]
if not strategy_enum:
strategy_enum = [1, 2, 3, 4]
strategy_item_schema: dict[str, Any] = {
"type": "object",
"additionalProperties": False,
"properties": {
"strategy": {"type": "integer", "enum": strategy_enum},
"decision": {"type": "string", "enum": ["use", "skip"]},
"entry": {"type": "number"},
"tp": {"type": "number"},
"sl": {"type": "number"},
"zone_low": {"type": "number"},
"zone_high": {"type": "number"},
"reason_code": {"type": "string", "enum": list(ENTRY_REASON_CODES)},
},
"required": [
"strategy",
"decision",
"entry",
"tp",
"sl",
"zone_low",
"zone_high",
"reason_code",
],
}
return {
"type": "json_schema",
"name": "hit_ea_entry_candidates",
"strict": True,
"schema": {
"type": "object",
"additionalProperties": False,
"properties": {
"schema_version": {"type": "integer", "enum": [ENTRY_RESPONSE_SCHEMA_VERSION]},
"strategies": {"type": "array", "items": strategy_item_schema},
},
"required": ["schema_version", "strategies"],
},
}
@@ -0,0 +1,31 @@
"""Debug log output tests."""
from __future__ import annotations
from pathlib import Path
from ea_py.io.debug_logs import append_debug_entry_stop
def test_append_debug_entry_stop_records_safe_stop_reason(tmp_path: Path) -> None:
"""Safe-stop diagnostics distinguish a guarded stop from a normal GPT skip."""
path = tmp_path / "debug_entry.txt"
append_debug_entry_stop(
path=path,
stage="openai_api",
reason="RateLimitError: retry later",
trend_state=2,
selected_strategies=[1, 2],
candidate_id="202606012100",
current_price=4535.17,
imbalance_summary="signal=NONE",
)
actual = path.read_text(encoding="utf-8")
assert "RESULT : SAFE_STOP" in actual
assert "STAGE : openai_api" in actual
assert "REASON : RateLimitError: retry later" in actual
assert "MARKET_STATE(H4) : 2 (LOW_VOL_UP)" in actual
assert "SELECTED_STRATEGY : 1,2" in actual
assert "CANDIDATE_ID : 202606012100" in actual
@@ -70,6 +70,36 @@ def test_call_responses_api_sends_reasoning_and_omits_temperature() -> None:
assert "temperature" not in fake_client.responses.create_params
def test_call_responses_api_sends_structured_text_format() -> None:
"""Structured Outputs用のtext.formatをResponses APIへ渡す。"""
fake_client = FakeClient(FakeResponse(output_text='{"schema_version":1,"strategies":[]}'))
response_text_format = {
"type": "json_schema",
"name": "entry_candidates",
"strict": True,
"schema": {
"type": "object",
"additionalProperties": False,
"properties": {"schema_version": {"type": "integer"}},
"required": ["schema_version"],
},
}
actual = call_responses_api(
client=fake_client, # type: ignore[arg-type]
model="gpt-5.5-2026-04-23",
reasoning_effort="low",
system_content="Return JSON.",
user_text="Classify.",
image_data_urls=[],
max_output_tokens=128,
response_text_format=response_text_format,
)
assert actual.text.startswith("{")
assert fake_client.responses.create_params["text"] == {"format": response_text_format}
def test_call_responses_api_returns_incomplete_diagnostics_for_empty_text() -> None:
"""Empty output_text keeps the API status details for live diagnosis."""
fake_client = FakeClient(
@@ -91,3 +121,4 @@ def test_call_responses_api_returns_incomplete_diagnostics_for_empty_text() -> N
assert actual.text == ""
assert "max_output_tokens" in actual.diagnostics.incomplete_details
assert not actual.diagnostics.is_completed()
@@ -6,6 +6,7 @@ from ea_py.market.target_prices import parse_lines_to_13_allow_subset, sanitize_
from ea_py.market.target_zones import (
build_candidate_id,
format_target_zones,
parse_json_to_entry_zones,
parse_lines_to_entry_zones_allow_subset,
sanitize_entry_zones,
zones_to_numeric_list,
@@ -74,6 +75,51 @@ def test_parse_lines_to_entry_zones_allow_subset_valid_zone_returns_zone_values(
assert actual[2].zone_high == 1898.0
def test_parse_json_to_entry_zones_valid_structured_output_returns_zone_values() -> None:
"""Structured OutputsのJSON候補を戦略別ゾーンへ展開する。"""
actual = parse_json_to_entry_zones(
"""
{
"schema_version": 1,
"strategies": [
{
"strategy": 2,
"decision": "use",
"entry": 1895.0,
"tp": 1910.0,
"sl": 1885.0,
"zone_low": 1892.0,
"zone_high": 1898.0,
"reason_code": "range_edge"
},
{
"strategy": 4,
"decision": "skip",
"entry": 0.0,
"tp": 0.0,
"sl": 0.0,
"zone_low": 0.0,
"zone_high": 0.0,
"reason_code": "skip_mid_range"
}
]
}
"""
)
assert actual[2].entry == 1895.0
assert actual[2].zone_low == 1892.0
assert actual[2].zone_high == 1898.0
assert actual[4].entry == 0.0
def test_parse_json_to_entry_zones_invalid_json_returns_stop_values() -> None:
"""不正JSONは全停止ゾーンへ倒す。"""
actual = parse_json_to_entry_zones("2,1895.00,1910.00,1885.00,1892.00,1898.00")
assert all(zone.entry == 0.0 for zone in actual.values())
def test_sanitize_entry_zones_invalid_unselected_strategy_zeroes_it() -> None:
"""未選択戦略のゾーンは停止値へ補正する。"""
parsed = parse_lines_to_entry_zones_allow_subset(
@@ -96,6 +142,20 @@ def test_sanitize_entry_zones_allows_near_zone_edge_when_entry_is_farther() -> N
assert actual[2].entry == 1890.0
def test_sanitize_entry_zones_rejects_low_reward_risk_ratio() -> None:
"""reward/riskが閾値未満の候補は停止値へ補正する。"""
parsed = parse_lines_to_entry_zones_allow_subset("2,1895.00,1900.00,1885.00,1892.00,1898.00")
actual = sanitize_entry_zones(
parsed,
selected_strategies=[2],
current_price=1900.0,
min_reward_risk_ratio=1.2,
)
assert actual[2].entry == 0.0
def test_zones_to_numeric_list_valid_zone_returns_legacy_13_values() -> None:
"""有効ゾーンのentry/tp/slから既存13行形式を生成する。"""
parsed = parse_lines_to_entry_zones_allow_subset("4,1910.00,1880.00,1920.00,1908.00,1912.00")