Merge pull request #1 from HirorihirorihK/codex/20260529
Codex/20260529
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
#define WAIT_OBJECT_0 0
|
||||
#define WAIT_TIMEOUT 258
|
||||
#define STILL_ACTIVE 259
|
||||
#define TASKKILL_WAIT_MILLISECONDS 5000
|
||||
|
||||
struct STARTUPINFO_W
|
||||
{
|
||||
@@ -66,12 +67,15 @@ long OpenProcess(uint dwDesiredAccess, int bInheritHandle, uint dwProcessId);
|
||||
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";
|
||||
@@ -182,6 +186,7 @@ struct EAState
|
||||
datetime last_trend_update; // ★追加:前回トレンドを更新した時刻
|
||||
datetime last_target_update; // ★変更:前回ターゲット価格を更新した時刻
|
||||
datetime target_loaded_at; // H1候補価格をEAへ読み込んだサーバー時刻
|
||||
datetime target_candidate_at; // H1候補価格の元になった確定足時刻
|
||||
datetime last_chk;
|
||||
};
|
||||
|
||||
@@ -264,6 +269,93 @@ 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);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| |
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -272,6 +364,8 @@ string PythonBatchPath(const string filename)
|
||||
*/
|
||||
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");
|
||||
|
||||
@@ -242,6 +242,84 @@ bool UpdateExternalProcessStatus(ExternalProcessState &process, const string lab
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| 外部プロセスツリーを強制終了する関数
|
||||
//+------------------------------------------------------------------+
|
||||
/**
|
||||
* @brief タイムアウトしたcmd/bat配下のPythonをプロセスツリーごと終了します。
|
||||
*
|
||||
* @param label ログ表示用ラベル。
|
||||
* @param process 終了対象の外部プロセス状態。
|
||||
* @return taskkill が正常終了した場合はtrue。
|
||||
*/
|
||||
bool TerminateExternalProcessTree(const string label, ExternalProcessState &process)
|
||||
{
|
||||
if(process.process_id == 0)
|
||||
{
|
||||
Print("[", label, "] cannot terminate process tree: pid is missing.");
|
||||
return false;
|
||||
}
|
||||
|
||||
STARTUPINFO_W startup_info = {};
|
||||
PROCESS_INFORMATION process_info = {};
|
||||
startup_info.cb = (uint)sizeof(startup_info);
|
||||
|
||||
string taskkill_exe = "C:\\Windows\\System32\\taskkill.exe";
|
||||
string command_line = "\"" + taskkill_exe + "\" /PID " +
|
||||
IntegerToString((long)process.process_id) + " /T /F";
|
||||
|
||||
int created = CreateProcessW(
|
||||
taskkill_exe,
|
||||
command_line,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
CREATE_NO_WINDOW,
|
||||
0,
|
||||
"C:\\Windows\\System32",
|
||||
startup_info,
|
||||
process_info
|
||||
);
|
||||
|
||||
if(created == 0 || process_info.hProcess == 0)
|
||||
{
|
||||
Print("[", label, "] taskkill launch failed. pid=", process.process_id,
|
||||
" err=", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if(process_info.hThread != 0)
|
||||
CloseHandle(process_info.hThread);
|
||||
|
||||
int wait_result = WaitForSingleObject(process_info.hProcess, TASKKILL_WAIT_MILLISECONDS);
|
||||
uint exit_code = 1;
|
||||
if(wait_result == WAIT_OBJECT_0)
|
||||
{
|
||||
if(GetExitCodeProcess(process_info.hProcess, exit_code) == 0)
|
||||
{
|
||||
Print("[", label, "] failed to get taskkill exit code. err=", GetLastError());
|
||||
exit_code = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Print("[", label, "] taskkill wait failed. result=", wait_result,
|
||||
" err=", GetLastError());
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hProcess);
|
||||
if(wait_result != WAIT_OBJECT_0 || exit_code != 0)
|
||||
{
|
||||
Print("[", label, "] taskkill failed. pid=", process.process_id,
|
||||
" exit_code=", exit_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
Print("[", label, "] timed-out Python process tree terminated. pid=", process.process_id);
|
||||
ResetExternalProcessState(process);
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| 外部プロセスがタイムアウトしているか判定する関数
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -308,7 +386,12 @@ string BatchWorkingDirectory(const string bat_file)
|
||||
//+------------------------------------------------------------------+
|
||||
//| |
|
||||
//+------------------------------------------------------------------+
|
||||
bool StartBatchProcess(const string bat_file, const string running_file, const string label, ExternalProcessState &process)
|
||||
bool StartBatchProcess(const string bat_file,
|
||||
const string running_file,
|
||||
const string label,
|
||||
ExternalProcessState &process,
|
||||
const string file_prefix,
|
||||
const int price_digits)
|
||||
{
|
||||
STARTUPINFO_W startup_info = {};
|
||||
PROCESS_INFORMATION process_info = {};
|
||||
@@ -316,7 +399,8 @@ bool StartBatchProcess(const string bat_file, const string running_file, const s
|
||||
startup_info.cb = (uint)sizeof(startup_info);
|
||||
|
||||
string cmd_exe = "C:\\Windows\\System32\\cmd.exe";
|
||||
string command_line = "\"" + cmd_exe + "\" /c \"\"" + bat_file + "\"\"";
|
||||
string command_line = "\"" + cmd_exe + "\" /c \"\"" + bat_file + "\" \"" +
|
||||
file_prefix + "\" \"" + IntegerToString(price_digits) + "\"\"";
|
||||
string current_directory = BatchWorkingDirectory(bat_file);
|
||||
|
||||
int created = CreateProcessW(
|
||||
@@ -353,7 +437,8 @@ bool StartBatchProcess(const string bat_file, const string running_file, const s
|
||||
|
||||
CreateRunningFile(running_file, process.process_id);
|
||||
Print("[", label, "] process started. pid=", process.process_id,
|
||||
" file=", bat_file, " cwd=", current_directory);
|
||||
" file=", bat_file, " cwd=", current_directory,
|
||||
" prefix=", file_prefix, " digits=", price_digits);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -546,8 +631,17 @@ bool RecoverTimedOutProcess(const string done_file, const string running_file, c
|
||||
if(!UpdateExternalProcessStatus(process, label))
|
||||
{
|
||||
if(IsExternalProcessTimedOut(process, running_file))
|
||||
{
|
||||
Print("[", label, "] Python exceeded ", PYTHON_TIMEOUT_SECONDS,
|
||||
" seconds, but the process is still running. Keep waiting.");
|
||||
" seconds. Terminating process tree before retry.");
|
||||
if(TerminateExternalProcessTree(label, process))
|
||||
{
|
||||
DeleteRunningFile(running_file);
|
||||
DeleteDoneFile(done_file);
|
||||
return true;
|
||||
}
|
||||
Print("[", label, "] process tree termination failed. Keep waiting.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,8 +80,9 @@ bool ValidateEntryPreconditions(EAState &state)
|
||||
|
||||
if(IsTargetCandidateExpired(state))
|
||||
{
|
||||
Print("[Entry Skip] H1 target candidate expired. loaded_at=",
|
||||
TimeToString(state.target_loaded_at, TIME_DATE | TIME_SECONDS));
|
||||
Print("[Entry Skip] H1 target candidate expired. candidate_at=",
|
||||
TimeToString(TargetCandidateReferenceTime(state), TIME_DATE | TIME_SECONDS),
|
||||
" loaded_at=", TimeToString(state.target_loaded_at, TIME_DATE | TIME_SECONDS));
|
||||
state.chk_cnt = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -91,6 +92,7 @@ bool ValidateEntryPreconditions(EAState &state)
|
||||
Print("[Entry Skip] H1 target candidate is too old for new execution. age=",
|
||||
TargetCandidateAgeText(state),
|
||||
" max_age_minutes=", input_entry_max_candidate_age_minutes,
|
||||
" candidate_at=", TimeToString(TargetCandidateReferenceTime(state), TIME_DATE | TIME_SECONDS),
|
||||
" loaded_at=", TimeToString(state.target_loaded_at, TIME_DATE | TIME_SECONDS));
|
||||
state.chk_cnt = 0;
|
||||
return false;
|
||||
@@ -113,19 +115,20 @@ bool ValidateEntryPreconditions(EAState &state)
|
||||
/**
|
||||
* @brief H1候補価格がENTRY_H1_LIMIT時間を超えて古くなっていないか判定します。
|
||||
*
|
||||
* @param state EA全体の状態。H1候補価格の読込時刻を参照します。
|
||||
* @param state EA全体の状態。H1候補価格の元になった確定足時刻を参照します。
|
||||
* @return 候補価格が期限切れの場合はtrue。
|
||||
*/
|
||||
bool IsTargetCandidateExpired(EAState &state)
|
||||
{
|
||||
if(state.target_loaded_at <= 0)
|
||||
datetime reference_time = TargetCandidateReferenceTime(state);
|
||||
if(reference_time <= 0)
|
||||
return false;
|
||||
|
||||
int expiration_seconds = ENTRY_H1_LIMIT * PeriodSeconds(PERIOD_H1);
|
||||
if(expiration_seconds <= 0)
|
||||
return false;
|
||||
|
||||
return (TimeCurrent() - state.target_loaded_at >= expiration_seconds);
|
||||
return (TimeCurrent() - reference_time >= expiration_seconds);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -134,14 +137,14 @@ bool IsTargetCandidateExpired(EAState &state)
|
||||
/**
|
||||
* @brief M15確認が遅れて整った古いH1候補で新規注文しないように判定します。
|
||||
*
|
||||
* @param state EA全体の状態。H1候補価格の読込時刻を参照します。
|
||||
* @param state EA全体の状態。H1候補価格の元になった確定足時刻を参照します。
|
||||
* @return 入力で指定した最大経過分数を超えている場合はtrue。
|
||||
*/
|
||||
bool IsTargetCandidateTooOldForExecution(EAState &state)
|
||||
{
|
||||
if(input_entry_max_candidate_age_minutes <= 0)
|
||||
return false;
|
||||
if(state.target_loaded_at <= 0)
|
||||
if(TargetCandidateReferenceTime(state) <= 0)
|
||||
return false;
|
||||
|
||||
int max_age_seconds = input_entry_max_candidate_age_minutes * 60;
|
||||
@@ -151,18 +154,33 @@ bool IsTargetCandidateTooOldForExecution(EAState &state)
|
||||
return (TargetCandidateAgeSeconds(state) > max_age_seconds);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| H1候補価格の鮮度判定に使う基準時刻を返す関数
|
||||
//+------------------------------------------------------------------+
|
||||
/**
|
||||
* @brief Python結果の読込時刻ではなく、H1確定足由来の候補時刻を優先します。
|
||||
*/
|
||||
datetime TargetCandidateReferenceTime(EAState &state)
|
||||
{
|
||||
if(state.target_candidate_at > 0)
|
||||
return state.target_candidate_at;
|
||||
|
||||
return state.target_loaded_at;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| H1候補価格の経過秒数を返す関数
|
||||
//+------------------------------------------------------------------+
|
||||
/**
|
||||
* @brief H1候補価格をEAへ読み込んでからの経過秒数を返します。
|
||||
* @brief H1候補価格の元になった確定足からの経過秒数を返します。
|
||||
*/
|
||||
int TargetCandidateAgeSeconds(EAState &state)
|
||||
{
|
||||
if(state.target_loaded_at <= 0)
|
||||
datetime reference_time = TargetCandidateReferenceTime(state);
|
||||
if(reference_time <= 0)
|
||||
return -1;
|
||||
|
||||
return (int)(TimeCurrent() - state.target_loaded_at);
|
||||
return (int)(TimeCurrent() - reference_time);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -671,7 +689,7 @@ bool IsM15EntryTimingConfirmed(const int orderType, TickContext &ctx, const doub
|
||||
if(!use_m15_entry_filter)
|
||||
return true;
|
||||
|
||||
if(IsTrendStopOrderType(orderType))
|
||||
if(IsTrendStopOrderType(orderType) && !use_m15_imbalance_confirmation)
|
||||
return true;
|
||||
|
||||
MqlRates rates[];
|
||||
@@ -685,6 +703,9 @@ bool IsM15EntryTimingConfirmed(const int orderType, TickContext &ctx, const doub
|
||||
int last_index = copied - 1;
|
||||
int prev_index = copied - 2;
|
||||
|
||||
if(IsTrendStopOrderType(orderType))
|
||||
return IsM15ImbalanceConfirmationPassed(orderType, rates, copied, last_index);
|
||||
|
||||
double avg_range = AverageM15Range(rates, copied);
|
||||
double min_zone = M15_MIN_ENTRY_ZONE_POINTS * Point();
|
||||
double entry_zone = avg_range * m15_entry_zone_atr_multiplier;
|
||||
@@ -719,7 +740,7 @@ bool IsM15ZoneTimingConfirmed(const int orderType,
|
||||
if(!use_m15_entry_filter)
|
||||
return true;
|
||||
|
||||
if(IsTrendStopOrderType(orderType))
|
||||
if(IsTrendStopOrderType(orderType) && !use_m15_imbalance_confirmation)
|
||||
return true;
|
||||
|
||||
MqlRates rates[];
|
||||
@@ -733,6 +754,9 @@ bool IsM15ZoneTimingConfirmed(const int orderType,
|
||||
int last_index = copied - 1;
|
||||
int prev_index = copied - 2;
|
||||
|
||||
if(IsTrendStopOrderType(orderType))
|
||||
return IsM15ImbalanceConfirmationPassed(orderType, rates, copied, last_index);
|
||||
|
||||
double avg_range = AverageM15Range(rates, copied);
|
||||
double min_zone = M15_MIN_ENTRY_ZONE_POINTS * Point();
|
||||
double entry_zone = avg_range * m15_entry_zone_atr_multiplier;
|
||||
|
||||
@@ -119,6 +119,166 @@ bool RecordOHLC(const string filename, const datetime ×[], const double &op
|
||||
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スクリプト)を実行する関数
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -135,7 +295,9 @@ bool ExecuteBatchTrend()
|
||||
return false;
|
||||
|
||||
DeleteDoneFile(done_trend_file);
|
||||
return StartBatchProcess(get_trend_reply_bat, running_trend_file, "trend", g_trend_process);
|
||||
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);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -154,7 +316,9 @@ bool ExecuteBatchEntry()
|
||||
return false;
|
||||
|
||||
DeleteDoneFile(done_entry_file);
|
||||
return StartBatchProcess(get_entry_reply_bat, running_entry_file, "entry", g_entry_process);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,7 +346,7 @@ bool RecordOHLCAndExecuteBatch_Trend(EAState &state)
|
||||
PERIOD_H4, HISTORY_BARS))
|
||||
{ Print("GetLatestOHLC(H4) failed."); return false; }
|
||||
|
||||
if(!RecordOHLC("ohlc_H4.csv", times, open_prices, high_prices, low_prices, close_prices))
|
||||
if(!RecordOHLC(ohlc_h4_file, times, open_prices, high_prices, low_prices, close_prices))
|
||||
{ Print("RecordOHLC(H4) failed."); return false; }
|
||||
|
||||
if(!ExecuteBatchTrend())
|
||||
@@ -216,7 +380,7 @@ bool RecordOHLCAndExecuteBatch_Entry(EAState &state)
|
||||
PERIOD_H1, HISTORY_BARS))
|
||||
{ Print("GetLatestOHLC(H1) failed."); return false; }
|
||||
|
||||
if(!RecordOHLC("ohlc_H1.csv", times, open_prices, high_prices, low_prices, close_prices))
|
||||
if(!RecordOHLC(ohlc_h1_file, times, open_prices, high_prices, low_prices, close_prices))
|
||||
{ Print("RecordOHLC(H1) failed."); return false; }
|
||||
|
||||
if(!ExecuteBatchEntry())
|
||||
@@ -272,8 +436,9 @@ void LoadTrendState(int &trend_state)
|
||||
if(filehandle != INVALID_HANDLE)
|
||||
{
|
||||
string line = FileReadString(filehandle);
|
||||
int value = (int)StringToInteger(line);
|
||||
if(value >= MARKET_LOW_VOL_RANGE && value <= MARKET_TECHNICAL_ERROR_STOP)
|
||||
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).");
|
||||
@@ -325,8 +490,13 @@ bool GetTargetPrices(EAState &state)
|
||||
" | 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 && state.zone_res_chk == 1)
|
||||
CancelStaleSplitPendingOrders(state.zone_candidate_id);
|
||||
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(); // ←変更
|
||||
@@ -359,17 +529,29 @@ void LoadTargetPrices(double &target_prices[])
|
||||
if(filehandle != INVALID_HANDLE)
|
||||
{
|
||||
int i = 0;
|
||||
bool parse_failed = false;
|
||||
while(!FileIsEnding(filehandle) && i < TARGET_SIZE)
|
||||
{
|
||||
string line = FileReadString(filehandle);
|
||||
target_prices[i] = StringToDouble(line);
|
||||
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(i < TARGET_SIZE)
|
||||
if(parse_failed || i < TARGET_SIZE)
|
||||
{
|
||||
Print("target_prices.txt line count is short. loaded=", i, " required=", 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; // 不完全なファイルは無効扱い
|
||||
}
|
||||
}
|
||||
@@ -391,6 +573,7 @@ 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++)
|
||||
{
|
||||
@@ -411,14 +594,26 @@ bool ParseTargetZoneLine(const string line, EAState &state, const int digits)
|
||||
if(count < 5)
|
||||
return false;
|
||||
|
||||
int strategy = (int)StringToInteger(parts[0]);
|
||||
int strategy = 0;
|
||||
if(!TryParseStrictInteger(parts[0], strategy))
|
||||
return false;
|
||||
if(strategy < 1 || strategy > 4)
|
||||
return false;
|
||||
|
||||
state.zone_low[strategy] = NormalizeDouble(StringToDouble(parts[1]), digits);
|
||||
state.zone_high[strategy] = NormalizeDouble(StringToDouble(parts[2]), digits);
|
||||
state.zone_tp[strategy] = NormalizeDouble(StringToDouble(parts[3]), digits);
|
||||
state.zone_sl[strategy] = NormalizeDouble(StringToDouble(parts[4]), digits);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -456,10 +651,11 @@ void LoadTargetZones(EAState &state)
|
||||
line_index++;
|
||||
if(line_index == 1)
|
||||
{
|
||||
int schema_version = (int)StringToInteger(line);
|
||||
if(schema_version != TARGET_ZONE_SCHEMA_VERSION)
|
||||
int schema_version = 0;
|
||||
if(!TryParseStrictInteger(line, schema_version) ||
|
||||
schema_version != TARGET_ZONE_SCHEMA_VERSION)
|
||||
{
|
||||
Print("target_zones schema mismatch. loaded=", schema_version,
|
||||
Print("target_zones schema mismatch. loaded=", line,
|
||||
" required=", TARGET_ZONE_SCHEMA_VERSION);
|
||||
FileClose(filehandle);
|
||||
ResetTargetZones(state);
|
||||
@@ -470,13 +666,27 @@ void LoadTargetZones(EAState &state)
|
||||
|
||||
if(line_index == 2)
|
||||
{
|
||||
state.zone_res_chk = (int)StringToInteger(line);
|
||||
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 = line;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -499,6 +709,7 @@ void LoadTargetZones(EAState &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],
|
||||
|
||||
@@ -2,6 +2,66 @@
|
||||
#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)
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -23,7 +83,7 @@ bool SendOrder(int orderType, double price, double tp, double sl, double volume,
|
||||
|
||||
request.magic = magic_number;
|
||||
request.symbol = _Symbol;
|
||||
request.volume = volume;
|
||||
request.volume = NormalizeDouble(volume, VolumeDigits());
|
||||
request.deviation = slippage;
|
||||
request.type_filling = GetOrderFillingPolicy(_Symbol);
|
||||
request.price = NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
|
||||
@@ -64,6 +124,12 @@ bool SendOrder(int orderType, double price, double tp, double sl, double volume,
|
||||
return false;
|
||||
}
|
||||
|
||||
string volume_context = orderTypeStr;
|
||||
if(comment_suffix != "")
|
||||
volume_context += " " + comment_suffix;
|
||||
if(!IsOrderVolumeAllowed(volume, volume_context))
|
||||
return false;
|
||||
|
||||
// 注文送信
|
||||
if(!OrderSend(request, result))
|
||||
{
|
||||
@@ -370,9 +436,7 @@ bool HasExistingSplitSlot(const string slot_key)
|
||||
bool CancelStaleSplitPendingOrders(const string current_candidate_id)
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
if(current_candidate_id == "" || current_candidate_id == "0")
|
||||
return false;
|
||||
bool keep_current_candidate = (current_candidate_id != "" && current_candidate_id != "0");
|
||||
|
||||
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
||||
{
|
||||
@@ -389,7 +453,7 @@ bool CancelStaleSplitPendingOrders(const string current_candidate_id)
|
||||
string comment = OrderGetString(ORDER_COMMENT);
|
||||
if(!IsSplitOrderComment(comment))
|
||||
continue;
|
||||
if(IsCurrentSplitCandidateComment(comment, current_candidate_id))
|
||||
if(keep_current_candidate && IsCurrentSplitCandidateComment(comment, current_candidate_id))
|
||||
continue;
|
||||
|
||||
MqlTradeRequest request = {};
|
||||
|
||||
@@ -335,8 +335,9 @@ public:
|
||||
continue;
|
||||
|
||||
double candidate_sl = 0.0;
|
||||
const double evaluation_price = (type == POSITION_TYPE_BUY) ? tick.bid : tick.ask;
|
||||
if (!CalcHighVolatilitySL(type,
|
||||
tick.bid,
|
||||
evaluation_price,
|
||||
open_m1,
|
||||
open_m3,
|
||||
open_m5,
|
||||
@@ -381,7 +382,10 @@ private:
|
||||
/// @details GOLD/XAUUSD は 0.1、2/3桁は0.01、4/5桁は0.0001を1 pipとして扱う。
|
||||
double PipSize()
|
||||
{
|
||||
if (m_symbol == "XAUUSD" || m_symbol == "GOLD")
|
||||
string normalized_symbol = m_symbol;
|
||||
StringToUpper(normalized_symbol);
|
||||
if (StringFind(normalized_symbol, "XAUUSD") >= 0 ||
|
||||
StringFind(normalized_symbol, "GOLD") >= 0)
|
||||
return 0.1;
|
||||
|
||||
const int digits = (int)SymbolInfoInteger(m_symbol, SYMBOL_DIGITS);
|
||||
@@ -585,7 +589,7 @@ private:
|
||||
/// @brief 高ボラティリティ判定に基づくSL候補を計算する。
|
||||
/// @return いずれかの時間足でしきい値を超え、SL候補を出力した場合は true。
|
||||
bool CalcHighVolatilitySL(const ENUM_POSITION_TYPE type,
|
||||
const double current_bid,
|
||||
const double current_price,
|
||||
const double open_m1,
|
||||
const double open_m3,
|
||||
const double open_m5,
|
||||
@@ -599,11 +603,11 @@ private:
|
||||
double candidate_sl = 0.0;
|
||||
const double rate_ratio = 0.9;
|
||||
|
||||
EvaluateHighVolatilityOpen(type, current_bid, open_m1, 100.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_bid, open_m3, 150.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_bid, open_m5, 200.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_bid, open_m10, 300.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_bid, open_m15, 400.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_price, open_m1, 100.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_price, open_m3, 150.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_price, open_m5, 200.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_price, open_m10, 300.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
EvaluateHighVolatilityOpen(type, current_price, open_m15, 400.0, rate_ratio, pip_size, digits, has_candidate, candidate_sl);
|
||||
|
||||
if (!has_candidate)
|
||||
return false;
|
||||
@@ -614,7 +618,7 @@ private:
|
||||
|
||||
/// @brief 1本の時間足始値から急変幅を評価し、有効なSL候補なら最良候補へ反映する。
|
||||
void EvaluateHighVolatilityOpen(const ENUM_POSITION_TYPE type,
|
||||
const double current_bid,
|
||||
const double current_price,
|
||||
const double open_price,
|
||||
const double limit_pips,
|
||||
const double rate_ratio,
|
||||
@@ -631,12 +635,12 @@ private:
|
||||
|
||||
if (type == POSITION_TYPE_BUY)
|
||||
{
|
||||
movement_pips = (current_bid - open_price) / pip_size;
|
||||
movement_pips = (current_price - open_price) / pip_size;
|
||||
sl_value = open_price + limit_pips * rate_ratio * pip_size;
|
||||
}
|
||||
else if (type == POSITION_TYPE_SELL)
|
||||
{
|
||||
movement_pips = (open_price - current_bid) / pip_size;
|
||||
movement_pips = (open_price - current_price) / pip_size;
|
||||
sl_value = open_price - limit_pips * rate_ratio * pip_size;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
- 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 the latest closed bar, the previous closed bar, average range, candle direction, rejection shape, and distance to the candidate entry price.
|
||||
- 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
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
- `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.
|
||||
@@ -28,15 +35,15 @@
|
||||
- `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 = 45`: Maximum age, in minutes, for using an H1 candidate for new execution. This blocks delayed entries when M15 confirmation appears too late.
|
||||
- `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 = 0.0`: Profit-side entry buffer used by 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 = 0.0`: Profit-side entry buffer used by elapsed-time break-even.
|
||||
- `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.
|
||||
@@ -50,7 +57,7 @@
|
||||
### Main Constants
|
||||
|
||||
- `POSITION_LIMIT = 48`: Maximum number of this EA's pending orders plus positions.
|
||||
- `ENTRY_H1_LIMIT = 1`: Pending-order lifetime in H1 bars.
|
||||
- `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.
|
||||
@@ -58,41 +65,46 @@
|
||||
- `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 `max(H1 EATR * 0.65, 5.00)`. A strategy beyond that limit is rewritten to `0.00,0.00,0.00`.
|
||||
- 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_ver5.mq5`: EA entry point. Keeps `OnInit`, `OnDeinit`, `OnTick`, inputs, global state, and DLL imports.
|
||||
- `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` loading.
|
||||
- `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: `ohlc_H4.csv`
|
||||
- Python output: `trend_state.txt`
|
||||
- Done marker: `process_done_trend.txt`
|
||||
- Running marker: `process_running_trend.txt`
|
||||
- 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: `ohlc_H1.csv`
|
||||
- Python output: `target_prices.txt`
|
||||
- Done marker: `process_done_entry.txt`
|
||||
- Running marker: `process_running_entry.txt`
|
||||
- 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`.
|
||||
@@ -107,6 +119,20 @@ 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:
|
||||
@@ -115,13 +141,14 @@ New entries are allowed only when all conditions below are satisfied:
|
||||
- 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`.
|
||||
- The H1 candidate set has not exceeded `input_entry_max_candidate_age_minutes` since it was loaded.
|
||||
- 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`, the M15 closed-bar timing confirmation is satisfied.
|
||||
- This EA's pending orders plus positions are below `POSITION_LIMIT`.
|
||||
- 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
|
||||
|
||||
@@ -131,7 +158,7 @@ New entries are allowed only when all conditions below are satisfied:
|
||||
- `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_ABNORMAL_VOL_STOP`: Blocks new entries.
|
||||
- `6 MARKET_TECHNICAL_ERROR_STOP`: Blocks new entries.
|
||||
|
||||
### Order-Type Price Rules
|
||||
|
||||
@@ -161,32 +188,57 @@ New entries are allowed only when all conditions below are satisfied:
|
||||
## 8. Risk Management
|
||||
|
||||
- Managed orders and positions are limited to matching `_Symbol` and `magic_number`.
|
||||
- `POSITION_LIMIT` counts only this EA's orders and positions, not the full account.
|
||||
- `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, or outside `0..6`, the EA uses `MARKET_ABNORMAL_VOL_STOP`.
|
||||
- If `target_prices.txt` is missing, unreadable, or shorter than 13 lines, the EA uses `res_chk = 0`.
|
||||
- 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 farther than `max(H1 EATR * 0.65, 5.00)` from the current price before they are passed to MT5.
|
||||
- Added `input_entry_max_candidate_age_minutes = 45` on the EA side so stale H1 candidates are not executed when M15 confirmation arrives late.
|
||||
- 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
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
- 標準インジケータおよびカスタムインジケータは未使用。
|
||||
- OHLCは `CopyRates(_Symbol, timeframe, OHLC_START_SHIFT, HISTORY_BARS, rates)` で取得する。
|
||||
- M15エントリー確認は、直近確定足と1本前の確定足のローソク足形状、平均レンジ、候補価格との距離から判定する。
|
||||
- M15エントリー確認は、T2/T4では直近確定足と1本前の確定足のローソク足形状、平均レンジ、候補価格との距離から判定し、T1/T3では必要に応じてM15初動確認を行う。
|
||||
|
||||
## 3. パラメータ設定
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
- `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 = 45`: H1候補価格を新規発注に使用できる最大経過分数。古い候補で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に丸めて使用する。
|
||||
@@ -40,10 +40,10 @@
|
||||
- `input_sltp_show_panel = true`: チャート上にSLTP操作パネルを表示するか。
|
||||
- `input_sltp_use_breakeven = false`: 通常ブレークイーブンを有効化するか。
|
||||
- `input_sltp_breakeven_trigger_pips = 30.0`: 通常ブレークイーブンを開始する含み益pips。
|
||||
- `input_sltp_breakeven_buffer_pips = 0.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 = 0.0`: 保有時間ベースのブレークイーブン時に建値から利益方向へずらすpips。
|
||||
- `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。
|
||||
@@ -57,7 +57,7 @@
|
||||
### 主要定数
|
||||
|
||||
- `POSITION_LIMIT = 48`: 自EAの未約定注文 + ポジション数の上限。
|
||||
- `ENTRY_H1_LIMIT = 1`: 未約定注文の有効期限(H1本数)。
|
||||
- `ENTRY_H1_LIMIT = 2`: 未約定注文の有効期限(H1本数)。
|
||||
- `CLOSE_H1_LIMIT = 12`: ポジションの時間制限クローズ(H1本数)。
|
||||
- `HISTORY_BARS = 72`: Pythonへ渡すOHLC本数。
|
||||
- `OHLC_START_SHIFT = 1`: 確定足からOHLCを取得するためのシフト。
|
||||
@@ -71,7 +71,7 @@
|
||||
### Python側エントリー候補ガード
|
||||
|
||||
- H1候補価格生成後、Python側で現在価格から遠すぎる `entry` を無効化する。
|
||||
- 距離上限は `max(H1 EATR * 0.65, 5.00)` とし、上限を超えた戦略は `0.00,0.00,0.00` に補正する。
|
||||
- 距離上限は戦略別に計算し、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. ファイル構成
|
||||
@@ -87,20 +87,24 @@
|
||||
|
||||
## 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出力: `ohlc_H4.csv`
|
||||
- Python出力: `trend_state.txt`
|
||||
- 完了フラグ: `process_done_trend.txt`
|
||||
- 実行中フラグ: `process_running_trend.txt`
|
||||
- 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出力: `ohlc_H1.csv`
|
||||
- Python出力: `target_prices.txt`, `target_zones.txt`
|
||||
- 完了フラグ: `process_done_entry.txt`
|
||||
- 実行中フラグ: `process_running_entry.txt`
|
||||
- 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` には依存しない。
|
||||
@@ -127,7 +131,7 @@ EAは `OnInit()` で `TerminalInfoString(TERMINAL_DATA_PATH)` から `MQL5\pytho
|
||||
7行目: T4 Sell Limit の strategy, zone_low, zone_high, tp, sl
|
||||
```
|
||||
|
||||
Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えてから `process_done_entry.txt` を作成する。分割エントリーが無効な場合、EAは従来どおり `target_prices.txt` を使用する。
|
||||
Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えてから `process_done_entry.txt` を作成する。`target_zones.txt` の価格小数桁は `MT5_PRICE_DIGITS` に合わせ、未指定時は安全側で5桁を使う。分割エントリーが無効な場合、EAは従来どおり `target_prices.txt` を使用する。
|
||||
|
||||
## 6. エントリー条件
|
||||
|
||||
@@ -137,12 +141,12 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
|
||||
- H4トレンド判定結果が完了済みで、`trend_state.txt` を読み込める。
|
||||
- H1候補価格生成が完了済みで、`target_prices.txt` を読み込める。
|
||||
- `res_chk = 1`。
|
||||
- H1候補価格が `ENTRY_H1_LIMIT` 内で期限切れではない。
|
||||
- H1候補価格の読込から `input_entry_max_candidate_age_minutes` 分を超えていない。
|
||||
- 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` の場合、M15確定足の方向・反発・接近条件を満たす。
|
||||
- `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 の重複注文/ポジション不存在を満たす。
|
||||
|
||||
@@ -154,7 +158,7 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
|
||||
- `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_ABNORMAL_VOL_STOP`: 新規注文を停止。
|
||||
- `6 MARKET_TECHNICAL_ERROR_STOP`: 新規注文を停止。
|
||||
|
||||
### 注文タイプごとの価格整合条件
|
||||
|
||||
@@ -186,22 +190,38 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
|
||||
- 注文/ポジション管理対象は `_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_ABNORMAL_VOL_STOP` として扱う。
|
||||
- `target_prices.txt` が存在しない、読み込めない、または13行未満の場合は `res_chk = 0` として扱う。
|
||||
- 分割エントリー有効時に `target_zones.txt` が存在しない、schema不一致、7行未満、または `res_chk != 1` の場合は新規注文を停止する。
|
||||
- `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` へ移行する仕様に変更した。
|
||||
@@ -217,8 +237,8 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて
|
||||
|
||||
### 2026-05-13
|
||||
|
||||
- Python側にH1 EATRベースの候補距離ガードを追加し、現在価格から `max(H1 EATR * 0.65, 5.00)` を超える候補をMT5へ渡す前に無効化する仕様を追加。
|
||||
- EA側に `input_entry_max_candidate_age_minutes = 45` を追加し、M15確認が遅れて整った古いH1候補で新規発注しないようにした。
|
||||
- Python側にH1 EATRベースの候補距離ガードを追加し、現在価格から遠すぎる候補をMT5へ渡す前に無効化する仕様を追加。
|
||||
- EA側に `input_entry_max_candidate_age_minutes` を追加し、M15確認が遅れて整った古いH1候補で新規発注しないようにした。
|
||||
- エントリー見送りログへH1候補の経過秒数を追加し、遅延・M15未確認・価格不整合の原因を追跡しやすくした。
|
||||
|
||||
### 2026-05-05
|
||||
|
||||
+17
-10
@@ -36,14 +36,16 @@ MT5 EA からは `bat/` 経由でルート直下の互換スクリプトを起
|
||||
|
||||
パスの組み立ては `src/ea_py/paths.py` で定義しています。特殊な配置では `MT5_FILES_DIR` または `MT5_DATA_PATH` 環境変数で上書きできます。
|
||||
|
||||
EAからbat経由で起動する場合は、第1引数で渡された `HIT_<symbol>_<magic_number>` 接頭辞が `MT5_EA_FILE_PREFIX` として設定されます。第2引数で渡された `_Symbol` の価格桁数は `MT5_PRICE_DIGITS` として設定されます。Pythonはこの接頭辞をすべての連携ファイル名へ付けるため、同じTerminal内で複数EAや複数シンボルを動かしてもファイルが衝突しません。手動実行で接頭辞が未設定の場合は、従来のファイル名を使います。
|
||||
|
||||
### 入力
|
||||
|
||||
```text
|
||||
ohlc_H4.csv
|
||||
ohlc_H1.csv
|
||||
[<prefix>_]ohlc_H4.csv
|
||||
[<prefix>_]ohlc_H1.csv
|
||||
```
|
||||
|
||||
CSV は UTF-8 読み込みで、次の列を必須とします。
|
||||
CSV は UTF-8 読み込みで、次の列を必須とします。価格列はfloatへ変換し、NaN/infや `High < Low`、High/LowがOpen/Closeを含まない行は停止値へ倒すため例外にします。
|
||||
|
||||
```text
|
||||
Time,Open,High,Low,Close
|
||||
@@ -52,11 +54,11 @@ Time,Open,High,Low,Close
|
||||
### 出力
|
||||
|
||||
```text
|
||||
trend_state.txt
|
||||
target_prices.txt
|
||||
target_zones.txt
|
||||
process_done_trend.txt
|
||||
process_done_entry.txt
|
||||
[<prefix>_]trend_state.txt
|
||||
[<prefix>_]target_prices.txt
|
||||
[<prefix>_]target_zones.txt
|
||||
[<prefix>_]process_done_trend.txt
|
||||
[<prefix>_]process_done_entry.txt
|
||||
```
|
||||
|
||||
MT5 が読む結果ファイルは `utf-16 LE` で出力します。結果ファイルを書き終えたあとに done ファイルを作成します。古い done ファイルは結果書き込み前に削除されます。
|
||||
@@ -81,7 +83,7 @@ MT5 が読む結果ファイルは `utf-16 LE` で出力します。結果ファ
|
||||
|
||||
## H1 エントリー候補生成
|
||||
|
||||
`get_entry_reply.py` は `trend_state.txt` と H1 OHLC から `target_prices.txt` を生成します。
|
||||
`get_entry_reply.py` は `trend_state.txt` と H1 OHLC から `target_prices.txt` と `target_zones.txt` を生成します。
|
||||
|
||||
処理の概要:
|
||||
|
||||
@@ -125,6 +127,8 @@ H4 状態ごとの許可戦略:
|
||||
|
||||
EA側で分割エントリーを有効にした場合は、この予測ゾーンを `split_entry_count` 本に分割してpending注文を出します。ロットは総量分割または1注文固定を選択できます。
|
||||
|
||||
`target_zones.txt` の価格小数桁は `MT5_PRICE_DIGITS` に合わせます。EA/bat経由では自動設定され、手動実行で未指定の場合は安全側で5桁を使います。
|
||||
|
||||
## OpenAI 設定
|
||||
|
||||
H1 エントリー候補生成では OpenAI API を使用します。
|
||||
@@ -140,9 +144,11 @@ $env:OPENAI_API_KEY = "..."
|
||||
```powershell
|
||||
$env:OPENAI_MODEL = "..."
|
||||
$env:OPENAI_REASONING_EFFORT = "low"
|
||||
$env:MT5_EA_FILE_PREFIX = "HIT_GOLD_10001"
|
||||
$env:MT5_PRICE_DIGITS = "2"
|
||||
```
|
||||
|
||||
`OPENAI_MODEL` 未設定時は `src/ea_py/constants.py` の `DEFAULT_GPT_MODEL` を使います。`OPENAI_REASONING_EFFORT` は `none`, `low`, `medium`, `high`, `xhigh` のいずれかです。
|
||||
`OPENAI_MODEL` 未設定時は `src/ea_py/constants.py` の `DEFAULT_GPT_MODEL` を使います。`OPENAI_REASONING_EFFORT` は `none`, `low`, `medium`, `high`, `xhigh` のいずれかです。`MT5_EA_FILE_PREFIX` と `MT5_PRICE_DIGITS` は手動実行時だけ指定すればよく、EA/bat経由では自動設定されます。
|
||||
|
||||
## 主なモジュール
|
||||
|
||||
@@ -156,6 +162,7 @@ src/ea_py/charting/candlestick.py ローソク足 PNG 生成
|
||||
src/ea_py/market/volatility.py True Range / EATR / 数値要約
|
||||
src/ea_py/market/trend_state.py H4 方向判定と market_state 合成
|
||||
src/ea_py/market/target_prices.py GPT 出力のパースと価格検証
|
||||
src/ea_py/market/target_zones.py 分割エントリー用ゾーン出力
|
||||
src/ea_py/market/imbalance.py H1 インバランス初動判定
|
||||
src/ea_py/openai_client.py OpenAI Responses API ラッパー
|
||||
src/ea_py/pipelines/ H4/H1 パイプライン
|
||||
|
||||
@@ -5,6 +5,8 @@ chcp 65001 >nul
|
||||
set PYTHONIOENCODING=utf-8
|
||||
set "APP_DIR=%~dp0.."
|
||||
set PY_FILE=get_entry_reply.py
|
||||
if not "%~1"=="" set "MT5_EA_FILE_PREFIX=%~1"
|
||||
if not "%~2"=="" set "MT5_PRICE_DIGITS=%~2"
|
||||
for %%I in ("%APP_DIR%") do set "APP_DIR=%%~fI"
|
||||
|
||||
cd /d "%APP_DIR%"
|
||||
|
||||
@@ -5,6 +5,8 @@ chcp 65001 >nul
|
||||
set PYTHONIOENCODING=utf-8
|
||||
set "APP_DIR=%~dp0.."
|
||||
set PY_FILE=get_trend_reply.py
|
||||
if not "%~1"=="" set "MT5_EA_FILE_PREFIX=%~1"
|
||||
if not "%~2"=="" set "MT5_PRICE_DIGITS=%~2"
|
||||
for %%I in ("%APP_DIR%") do set "APP_DIR=%%~fI"
|
||||
|
||||
cd /d "%APP_DIR%"
|
||||
|
||||
@@ -35,9 +35,11 @@ MT5 EAから `bat/` 経由でルート直下のPythonスクリプトが呼ばれ
|
||||
│ │ └─ candlestick.py
|
||||
│ ├─ market/
|
||||
│ │ ├─ __init__.py
|
||||
│ │ ├─ imbalance.py
|
||||
│ │ ├─ target_prices.py
|
||||
│ │ ├─ target_zones.py
|
||||
│ │ ├─ volatility.py
|
||||
│ │ ├─ trend_state.py
|
||||
│ │ └─ target_prices.py
|
||||
│ │ └─ trend_state.py
|
||||
│ ├─ prompts/
|
||||
│ │ ├─ __init__.py
|
||||
│ │ ├─ trend_prompt.py
|
||||
@@ -49,8 +51,12 @@ MT5 EAから `bat/` 経由でルート直下のPythonスクリプトが呼ばれ
|
||||
│ └─ entry_pipeline.py
|
||||
├─ tests/
|
||||
│ └─ unit/
|
||||
│ ├─ test_trend_state.py
|
||||
│ ├─ test_config.py
|
||||
│ ├─ test_imbalance.py
|
||||
│ ├─ test_openai_client.py
|
||||
│ ├─ test_paths.py
|
||||
│ ├─ test_target_prices.py
|
||||
│ ├─ test_trend_state.py
|
||||
│ └─ test_volatility.py
|
||||
├─ bat/
|
||||
│ ├─ get_trend_reply.bat
|
||||
@@ -86,23 +92,26 @@ MT5 EAから `bat/` 経由でルート直下のPythonスクリプトが呼ばれ
|
||||
|
||||
MT5側の `MQL5\Files` 配下に、Pythonとの連携ファイルが置かれる。
|
||||
|
||||
EA/bat経由では `HIT_<symbol>_<magic_number>` 形式の接頭辞が `MT5_EA_FILE_PREFIX` としてPythonへ渡され、すべての連携ファイル名へ付与される。さらに `_Symbol` の価格桁数が `MT5_PRICE_DIGITS` として渡され、`target_zones.txt` の価格出力桁数に使われる。接頭辞が未設定の手動実行では旧ファイル名を維持し、価格桁数が未指定の場合は5桁を使う。
|
||||
|
||||
代表例:
|
||||
|
||||
```text
|
||||
C:/Users/new/AppData/Roaming/MetaQuotes/Terminal/{terminal_ID}/MQL5/Files/
|
||||
├─ ohlc_H4.csv
|
||||
├─ ohlc_H1.csv
|
||||
├─ trend_state.txt
|
||||
├─ target_prices.txt
|
||||
├─ process_done_trend.txt
|
||||
├─ process_done_entry.txt
|
||||
├─ process_running_trend.txt
|
||||
├─ process_running_entry.txt
|
||||
├─ debug_trend.txt
|
||||
├─ debug_entry.txt
|
||||
├─ tmp_chart_trend.png
|
||||
├─ tmp_chart_short.png
|
||||
└─ tmp_chart_long.png
|
||||
├─ HIT_GOLD_10001_ohlc_H4.csv
|
||||
├─ HIT_GOLD_10001_ohlc_H1.csv
|
||||
├─ HIT_GOLD_10001_trend_state.txt
|
||||
├─ HIT_GOLD_10001_target_prices.txt
|
||||
├─ HIT_GOLD_10001_target_zones.txt
|
||||
├─ HIT_GOLD_10001_process_done_trend.txt
|
||||
├─ HIT_GOLD_10001_process_done_entry.txt
|
||||
├─ HIT_GOLD_10001_process_running_trend.txt
|
||||
├─ HIT_GOLD_10001_process_running_entry.txt
|
||||
├─ HIT_GOLD_10001_debug_trend.txt
|
||||
├─ HIT_GOLD_10001_debug_entry.txt
|
||||
├─ HIT_GOLD_10001_tmp_chart_trend.png
|
||||
├─ HIT_GOLD_10001_tmp_chart_short.png
|
||||
└─ HIT_GOLD_10001_tmp_chart_long.png
|
||||
```
|
||||
|
||||
これらは実行時生成物であり、原則として `MQL5\python_for_ea` 配下へコピーして正本化しない。
|
||||
@@ -115,7 +124,7 @@ MQL5 EAファイルは、MT5データフォルダ配下に存在する。
|
||||
|
||||
```text
|
||||
C:/Users/new/AppData/Roaming/MetaQuotes/Terminal/{terminal_ID}/MQL5/Experts/MyProject/
|
||||
└─ HIT-EA_refactor_ver3.mq5
|
||||
└─ HIT-EA_refactor_ver6.mq5
|
||||
```
|
||||
|
||||
EAファイルはこのPythonプロジェクトの外部連携先として扱う。Python側の出力形式を変更する場合は、EA側の読込処理も同時に確認する。
|
||||
@@ -154,9 +163,11 @@ EAファイルはこのPythonプロジェクトの外部連携先として扱う
|
||||
│ │ └─ candlestick.py
|
||||
│ ├─ market/
|
||||
│ │ ├─ __init__.py
|
||||
│ │ ├─ imbalance.py
|
||||
│ │ ├─ target_prices.py
|
||||
│ │ ├─ target_zones.py
|
||||
│ │ ├─ volatility.py
|
||||
│ │ ├─ trend_state.py
|
||||
│ │ └─ target_prices.py
|
||||
│ │ └─ trend_state.py
|
||||
│ ├─ prompts/
|
||||
│ │ ├─ __init__.py
|
||||
│ │ ├─ trend_prompt.py
|
||||
@@ -168,8 +179,12 @@ EAファイルはこのPythonプロジェクトの外部連携先として扱う
|
||||
│ └─ entry_pipeline.py
|
||||
├─ tests/
|
||||
│ └─ unit/
|
||||
│ ├─ test_trend_state.py
|
||||
│ ├─ test_config.py
|
||||
│ ├─ test_imbalance.py
|
||||
│ ├─ test_openai_client.py
|
||||
│ ├─ test_paths.py
|
||||
│ ├─ test_target_prices.py
|
||||
│ ├─ test_trend_state.py
|
||||
│ └─ test_volatility.py
|
||||
└─ docs/
|
||||
├─ architecture/
|
||||
@@ -193,6 +208,8 @@ EAファイルはこのPythonプロジェクトの外部連携先として扱う
|
||||
| `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/imbalance.py` | H1インバランス初動判定。 |
|
||||
| `src/ea_py/prompts/trend_prompt.py` | H4相場環境判定プロンプトの生成。 |
|
||||
| `src/ea_py/prompts/entry_prompt.py` | H1候補価格生成プロンプトの生成。 |
|
||||
| `src/ea_py/openai_client.py` | OpenAI API呼び出しの薄いラッパー。 |
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
@@ -9,6 +10,7 @@ import pandas as pd
|
||||
from ea_py.types import OhlcBar
|
||||
|
||||
REQUIRED_COLUMNS = ("Time", "Open", "High", "Low", "Close")
|
||||
PRICE_COLUMNS = ("Open", "High", "Low", "Close")
|
||||
|
||||
|
||||
def read_ohlc_csv(path: Path) -> list[OhlcBar]:
|
||||
@@ -18,7 +20,8 @@ def read_ohlc_csv(path: Path) -> list[OhlcBar]:
|
||||
`Time` は文字列、価格列はfloatへ変換し、内部表現では既存プロンプトと
|
||||
チャート生成処理に合わせて `Time` を `DateTime` キーへ写す。
|
||||
|
||||
必須列が欠けている場合や価格列のfloat変換に失敗した場合は例外を送出する。
|
||||
必須列が欠けている場合、価格列のfloat変換に失敗した場合、
|
||||
NaN/infやOHLC整合性違反がある場合は例外を送出する。
|
||||
上位パイプラインはその例外を捕捉し、MT5へ停止値を返す。
|
||||
"""
|
||||
df = pd.read_csv(path, encoding="utf-8")
|
||||
@@ -34,14 +37,31 @@ def read_ohlc_csv(path: Path) -> list[OhlcBar]:
|
||||
df["Close"] = df["Close"].astype(float)
|
||||
|
||||
ohlc: list[OhlcBar] = []
|
||||
for _, row in df.iterrows():
|
||||
for index, row in df.iterrows():
|
||||
prices = {column: float(row[column]) for column in PRICE_COLUMNS}
|
||||
invalid_columns = [column for column, value in prices.items() if not math.isfinite(value)]
|
||||
if invalid_columns:
|
||||
joined = ", ".join(invalid_columns)
|
||||
raise ValueError(f"OHLC CSV row {index} has non-finite price(s): {joined}")
|
||||
|
||||
high = prices["High"]
|
||||
low = prices["Low"]
|
||||
open_price = prices["Open"]
|
||||
close_price = prices["Close"]
|
||||
if high < low:
|
||||
raise ValueError(f"OHLC CSV row {index} has High < Low")
|
||||
if high < max(open_price, close_price):
|
||||
raise ValueError(f"OHLC CSV row {index} has High below Open/Close")
|
||||
if low > min(open_price, close_price):
|
||||
raise ValueError(f"OHLC CSV row {index} has Low above Open/Close")
|
||||
|
||||
ohlc.append(
|
||||
{
|
||||
"DateTime": row["Time"],
|
||||
"Open": float(row["Open"]),
|
||||
"High": float(row["High"]),
|
||||
"Low": float(row["Low"]),
|
||||
"Close": float(row["Close"]),
|
||||
"Open": open_price,
|
||||
"High": high,
|
||||
"Low": low,
|
||||
"Close": close_price,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ DEFAULT_USER_NAME = "new"
|
||||
DEFAULT_TERMINAL_ID = "5BDB0B60344C088C2FA5CA35699BAAFD"
|
||||
ENV_MT5_FILES_DIR = "MT5_FILES_DIR"
|
||||
ENV_MT5_DATA_PATH = "MT5_DATA_PATH"
|
||||
ENV_MT5_EA_FILE_PREFIX = "MT5_EA_FILE_PREFIX"
|
||||
ENV_MT5_PRICE_DIGITS = "MT5_PRICE_DIGITS"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -21,6 +23,8 @@ class Mt5PathSettings:
|
||||
data_path: Path | None = None
|
||||
user_name: str | None = None
|
||||
terminal_id: str | None = None
|
||||
file_prefix: str | None = None
|
||||
price_digits: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -68,6 +72,63 @@ def files_dir_from_data_path(data_path: Path) -> Path:
|
||||
return resolved / "MQL5" / "Files"
|
||||
|
||||
|
||||
def sanitize_file_prefix(prefix: str) -> str:
|
||||
"""EAから渡されたファイル接頭辞を安全なファイル名トークンへ丸める。"""
|
||||
stripped = prefix.strip()
|
||||
if not stripped:
|
||||
return ""
|
||||
|
||||
safe_chars = []
|
||||
for char in stripped:
|
||||
if (char.isascii() and char.isalnum()) or char in {"_", "-"}:
|
||||
safe_chars.append(char)
|
||||
else:
|
||||
safe_chars.append("_")
|
||||
|
||||
return "".join(safe_chars)
|
||||
|
||||
|
||||
def mt5_file_prefix(settings: Mt5PathSettings | None = None) -> str:
|
||||
"""MT5連携ファイル名へ付けるEA固有接頭辞を返す。"""
|
||||
if settings and settings.file_prefix is not None:
|
||||
return sanitize_file_prefix(settings.file_prefix)
|
||||
|
||||
return sanitize_file_prefix(os.getenv(ENV_MT5_EA_FILE_PREFIX, ""))
|
||||
|
||||
|
||||
def prefixed_file_name(base_name: str, settings: Mt5PathSettings | None = None) -> str:
|
||||
"""接頭辞がある場合だけ `prefix_base_name` 形式にする。"""
|
||||
prefix = mt5_file_prefix(settings)
|
||||
if not prefix:
|
||||
return base_name
|
||||
|
||||
return f"{prefix}_{base_name}"
|
||||
|
||||
|
||||
def sanitize_price_digits(value: int | str | None) -> int | None:
|
||||
"""MT5から渡された価格桁数を安全な範囲へ丸める。"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
digits = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
if 0 <= digits <= 10:
|
||||
return digits
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def mt5_price_digits(settings: Mt5PathSettings | None = None) -> int | None:
|
||||
"""MT5連携価格の小数桁数を返す。未指定や不正値はNone。"""
|
||||
if settings and settings.price_digits is not None:
|
||||
return sanitize_price_digits(settings.price_digits)
|
||||
|
||||
return sanitize_price_digits(os.getenv(ENV_MT5_PRICE_DIGITS))
|
||||
|
||||
|
||||
def terminal_files_dir(settings: Mt5PathSettings | None = None) -> Path:
|
||||
"""MT5のMQL5/Filesディレクトリを返す。
|
||||
|
||||
@@ -127,11 +188,11 @@ def build_trend_paths(settings: Mt5PathSettings | None = None) -> TrendPaths:
|
||||
"""H4トレンド判定用の入出力パスをまとめて返す。"""
|
||||
base_dir = terminal_files_dir(settings)
|
||||
return TrendPaths(
|
||||
input_csv=base_dir / "ohlc_H4.csv",
|
||||
trend_state=base_dir / "trend_state.txt",
|
||||
done_trend=base_dir / "process_done_trend.txt",
|
||||
tmp_chart=base_dir / "tmp_chart_trend.png",
|
||||
debug_reason=base_dir / "debug_trend.txt",
|
||||
input_csv=base_dir / prefixed_file_name("ohlc_H4.csv", settings),
|
||||
trend_state=base_dir / prefixed_file_name("trend_state.txt", settings),
|
||||
done_trend=base_dir / prefixed_file_name("process_done_trend.txt", settings),
|
||||
tmp_chart=base_dir / prefixed_file_name("tmp_chart_trend.png", settings),
|
||||
debug_reason=base_dir / prefixed_file_name("debug_trend.txt", settings),
|
||||
)
|
||||
|
||||
|
||||
@@ -139,12 +200,12 @@ def build_entry_paths(settings: Mt5PathSettings | None = None) -> EntryPaths:
|
||||
"""H1エントリー候補生成用の入出力パスをまとめて返す。"""
|
||||
base_dir = terminal_files_dir(settings)
|
||||
return EntryPaths(
|
||||
input_csv=base_dir / "ohlc_H1.csv",
|
||||
output_prices=base_dir / "target_prices.txt",
|
||||
output_zones=base_dir / "target_zones.txt",
|
||||
trend_state=base_dir / "trend_state.txt",
|
||||
done_entry=base_dir / "process_done_entry.txt",
|
||||
tmp_short_chart=base_dir / "tmp_chart_short.png",
|
||||
tmp_long_chart=base_dir / "tmp_chart_long.png",
|
||||
debug_reason=base_dir / "debug_entry.txt",
|
||||
input_csv=base_dir / prefixed_file_name("ohlc_H1.csv", settings),
|
||||
output_prices=base_dir / prefixed_file_name("target_prices.txt", settings),
|
||||
output_zones=base_dir / prefixed_file_name("target_zones.txt", settings),
|
||||
trend_state=base_dir / prefixed_file_name("trend_state.txt", settings),
|
||||
done_entry=base_dir / prefixed_file_name("process_done_entry.txt", settings),
|
||||
tmp_short_chart=base_dir / prefixed_file_name("tmp_chart_short.png", settings),
|
||||
tmp_long_chart=base_dir / prefixed_file_name("tmp_chart_long.png", settings),
|
||||
debug_reason=base_dir / prefixed_file_name("debug_entry.txt", settings),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""OHLC CSV reader tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from ea_py.io.ohlc_csv import read_ohlc_csv
|
||||
|
||||
|
||||
def write_csv(path: Path, body: str) -> None:
|
||||
"""Write a small MT5-style OHLC CSV for tests."""
|
||||
path.write_text("Time,Open,High,Low,Close\n" + body, encoding="utf-8")
|
||||
|
||||
|
||||
def test_read_ohlc_csv_rejects_non_finite_prices(tmp_path: Path) -> None:
|
||||
"""NaN/inf values are rejected before market calculations."""
|
||||
csv_path = tmp_path / "ohlc.csv"
|
||||
write_csv(csv_path, "2026.05.24 13:00,1900.0,nan,1890.0,1895.0\n")
|
||||
|
||||
with pytest.raises(ValueError, match="non-finite"):
|
||||
read_ohlc_csv(csv_path)
|
||||
|
||||
|
||||
def test_read_ohlc_csv_rejects_inconsistent_high_low(tmp_path: Path) -> None:
|
||||
"""High/Low must contain Open and Close."""
|
||||
csv_path = tmp_path / "ohlc.csv"
|
||||
write_csv(csv_path, "2026.05.24 13:00,1900.0,1899.0,1890.0,1895.0\n")
|
||||
|
||||
with pytest.raises(ValueError, match="High below Open/Close"):
|
||||
read_ohlc_csv(csv_path)
|
||||
|
||||
|
||||
def test_read_ohlc_csv_accepts_valid_mt5_rows(tmp_path: Path) -> None:
|
||||
"""Valid MT5 rows are converted to the internal DateTime schema."""
|
||||
csv_path = tmp_path / "ohlc.csv"
|
||||
write_csv(csv_path, "2026.05.24 13:00,1900.0,1902.0,1890.0,1895.0\n")
|
||||
|
||||
actual = read_ohlc_csv(csv_path)
|
||||
|
||||
assert actual == [
|
||||
{
|
||||
"DateTime": "2026.05.24 13:00",
|
||||
"Open": 1900.0,
|
||||
"High": 1902.0,
|
||||
"Low": 1890.0,
|
||||
"Close": 1895.0,
|
||||
}
|
||||
]
|
||||
@@ -7,11 +7,19 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from ea_py.paths import (
|
||||
ENV_MT5_EA_FILE_PREFIX,
|
||||
ENV_MT5_DATA_PATH,
|
||||
ENV_MT5_FILES_DIR,
|
||||
ENV_MT5_PRICE_DIGITS,
|
||||
Mt5PathSettings,
|
||||
build_entry_paths,
|
||||
build_trend_paths,
|
||||
discover_mql5_dir,
|
||||
files_dir_from_data_path,
|
||||
mt5_price_digits,
|
||||
prefixed_file_name,
|
||||
sanitize_file_prefix,
|
||||
sanitize_price_digits,
|
||||
terminal_files_dir,
|
||||
)
|
||||
|
||||
@@ -89,3 +97,66 @@ def test_terminal_files_dir_accepts_environment_data_path(
|
||||
actual = terminal_files_dir()
|
||||
|
||||
assert actual == data_path / "MQL5" / "Files"
|
||||
|
||||
|
||||
def test_prefixed_file_name_uses_legacy_name_when_prefix_is_empty(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""接頭辞未指定時は既存のMT5連携ファイル名を維持する。"""
|
||||
monkeypatch.delenv(ENV_MT5_EA_FILE_PREFIX, raising=False)
|
||||
|
||||
assert prefixed_file_name("target_prices.txt") == "target_prices.txt"
|
||||
|
||||
|
||||
def test_sanitize_file_prefix_replaces_unsafe_characters() -> None:
|
||||
"""Symbols containing dots or suffixes are safe for filesystem names."""
|
||||
assert sanitize_file_prefix("HIT_XAUUSD.pro_10001") == "HIT_XAUUSD_pro_10001"
|
||||
|
||||
|
||||
def test_build_paths_apply_file_prefix_from_settings(tmp_path: Path) -> None:
|
||||
"""同一Files配下でEAごとの連携ファイル名を分離できる。"""
|
||||
settings = Mt5PathSettings(files_dir=tmp_path, file_prefix="HIT_GOLD_10001")
|
||||
|
||||
trend_paths = build_trend_paths(settings)
|
||||
entry_paths = build_entry_paths(settings)
|
||||
|
||||
assert trend_paths.input_csv == tmp_path / "HIT_GOLD_10001_ohlc_H4.csv"
|
||||
assert trend_paths.trend_state == tmp_path / "HIT_GOLD_10001_trend_state.txt"
|
||||
assert entry_paths.input_csv == tmp_path / "HIT_GOLD_10001_ohlc_H1.csv"
|
||||
assert entry_paths.output_prices == tmp_path / "HIT_GOLD_10001_target_prices.txt"
|
||||
assert entry_paths.output_zones == tmp_path / "HIT_GOLD_10001_target_zones.txt"
|
||||
|
||||
|
||||
def test_build_paths_apply_file_prefix_from_environment(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""batから渡されたMT5_EA_FILE_PREFIXでPython側ファイル名を合わせる。"""
|
||||
monkeypatch.setenv(ENV_MT5_FILES_DIR, str(tmp_path))
|
||||
monkeypatch.setenv(ENV_MT5_EA_FILE_PREFIX, "HIT_GOLD_10001")
|
||||
|
||||
actual = build_entry_paths()
|
||||
|
||||
assert actual.done_entry == tmp_path / "HIT_GOLD_10001_process_done_entry.txt"
|
||||
|
||||
|
||||
def test_sanitize_price_digits_accepts_mt5_symbol_digits() -> None:
|
||||
"""MT5のSYMBOL_DIGITSは安全な範囲だけ受け入れる。"""
|
||||
assert sanitize_price_digits("3") == 3
|
||||
assert sanitize_price_digits(10) == 10
|
||||
assert sanitize_price_digits("-1") is None
|
||||
assert sanitize_price_digits("abc") is None
|
||||
|
||||
|
||||
def test_mt5_price_digits_reads_environment(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""EA/batから渡された価格桁数をPython側で参照できる。"""
|
||||
monkeypatch.setenv(ENV_MT5_PRICE_DIGITS, "3")
|
||||
|
||||
assert mt5_price_digits() == 3
|
||||
|
||||
|
||||
def test_mt5_price_digits_accepts_settings() -> None:
|
||||
"""テストや手動実行では設定オブジェクトから価格桁数を渡せる。"""
|
||||
settings = Mt5PathSettings(price_digits=2)
|
||||
|
||||
assert mt5_price_digits(settings) == 2
|
||||
|
||||
@@ -111,7 +111,7 @@ def test_format_target_zones_includes_schema_candidate_and_all_strategies() -> N
|
||||
parsed = parse_lines_to_entry_zones_allow_subset("2,1895.00,1910.00,1885.00,1892.00,1898.00")
|
||||
sanitized = sanitize_entry_zones(parsed, selected_strategies=[2], current_price=1900.0)
|
||||
|
||||
actual = format_target_zones(sanitized, "202605241300")
|
||||
actual = format_target_zones(sanitized, "202605241300", price_digits=2)
|
||||
|
||||
assert actual.splitlines() == [
|
||||
"2",
|
||||
@@ -124,6 +124,16 @@ def test_format_target_zones_includes_schema_candidate_and_all_strategies() -> N
|
||||
]
|
||||
|
||||
|
||||
def test_format_target_zones_uses_mt5_price_digits() -> None:
|
||||
"""MT5から渡された価格桁数に合わせてゾーン価格を出力する。"""
|
||||
parsed = parse_lines_to_entry_zones_allow_subset("2,1895.1234,1910.5678,1885.2345,1892.1234,1898.5678")
|
||||
sanitized = sanitize_entry_zones(parsed, selected_strategies=[2], current_price=1900.0)
|
||||
|
||||
actual = format_target_zones(sanitized, "202605241300", price_digits=3)
|
||||
|
||||
assert actual.splitlines()[4] == "2,1892.123,1898.568,1910.568,1885.235"
|
||||
|
||||
|
||||
def test_build_candidate_id_datetime_text_returns_compact_digits() -> None:
|
||||
"""H1確定足時刻からスペースなしの候補IDを作る。"""
|
||||
actual = build_candidate_id("2026.05.24 13:00")
|
||||
|
||||
Reference in New Issue
Block a user