#ifndef HIT_PYTHON_SIGNAL_GATEWAY_MQH #define HIT_PYTHON_SIGNAL_GATEWAY_MQH //+------------------------------------------------------------------+ //| 最新のOHLCデータを取得する関数 //+------------------------------------------------------------------+ /** * @brief 指定時間足のOHLCデータを取得します。 * * @param times 取得したバー時刻を格納する配列。 * @param open_prices 取得したOpen価格を格納する配列。 * @param high_prices 取得したHigh価格を格納する配列。 * @param low_prices 取得したLow価格を格納する配列。 * @param close_prices 取得したClose価格を格納する配列。 * @param tf 取得対象の時間足。 * @param bars_count 取得するバー本数。 * @return 取得に成功した場合はtrue、失敗した場合はfalse。 * * `OHLC_START_SHIFT=1` のため、形成中のバーではなく確定足から取得します。 */ bool GetLatestOHLC(datetime ×[], double &open_prices[], double &high_prices[], double &low_prices[], double &close_prices[], ENUM_TIMEFRAMES tf, int bars_count) { ArrayResize(times, 0); ArrayResize(open_prices, 0); ArrayResize(high_prices, 0); ArrayResize(low_prices, 0); ArrayResize(close_prices, 0); if(bars_count < 2) return false; // OHLCデータを取得 // CopyTime と CopyRates を別々に呼ばず、MqlRates の time/open/high/low/close を同一配列から取得する。 // OHLC_START_SHIFT=1 のため、形成中の0本目ではなく確定足からPythonへ渡す。 MqlRates rates[]; int copied = CopyRates(_Symbol, tf, OHLC_START_SHIFT, bars_count, rates); if(copied <= 0) { Print(__FUNCTION__, ": Failed to copy rates data (bars=", bars_count, ") err=", GetLastError()); return false; } ArrayResize(times, copied); ArrayResize(open_prices, copied); ArrayResize(high_prices, copied); ArrayResize(low_prices, copied); ArrayResize(close_prices, copied); // 必要なデータを配列に格納 for(int i = 0; i < copied; i++) { times[i] = rates[i].time; open_prices[i] = rates[i].open; high_prices[i] = rates[i].high; low_prices[i] = rates[i].low; close_prices[i] = rates[i].close; } return true; } //+------------------------------------------------------------------+ //| "ohlc.csv"を出力する関数 //+------------------------------------------------------------------+ /** * @brief OHLC配列をPython入力用CSVとして出力します。 * * @param filename 出力するCSVファイル名。 * @param times バー時刻配列。 * @param open_prices Open価格配列。 * @param high_prices High価格配列。 * @param low_prices Low価格配列。 * @param close_prices Close価格配列。 * @return CSV出力に成功した場合はtrue。 */ bool RecordOHLC(const string filename, const datetime ×[], const double &open_prices[], const double &high_prices[], const double &low_prices[], const double &close_prices[]) { // 出力先ファイル名 // string filename = "ohlc.csv"; // ファイルを "書き込みモード" でオープン (テキスト/ANSI) int fileHandle = FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI); // ファイルが開けたかチェック if(fileHandle == INVALID_HANDLE) { Print(__FUNCTION__, " : Failed to open file: ", GetLastError()); return false; } // 配列サイズを取得 (times, open, high, low, close の要素数は同じ前提) int size = ArraySize(times); // ヘッダー行を追加 FileWrite(fileHandle, "Time,Open,High,Low,Close"); // 小数点の桁数を取得 int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); // フォーマット文字列を動的に生成 string formatString = StringFormat("%%s,%%.%df,%%.%df,%%.%df,%%.%df", digits, digits, digits, digits); // 1行ずつ "時刻, Open, High, Low, Close" の形式で書き込み for(int i = 0; i < size; i++) { // 時間をフォーマット string timeStr = TimeToString(times[i], TIME_DATE | TIME_MINUTES); // 1行分の文字列を生成 (小数点以下の桁数を `digits` に調整) string line = StringFormat(formatString, timeStr, open_prices[i], high_prices[i], low_prices[i], close_prices[i]); // ファイルに書き込み (改行付き) FileWrite(fileHandle, line); } // 書き込み終了後、ファイルを閉じる FileClose(fileHandle); return true; } //+------------------------------------------------------------------+ //| 文字列の前後空白を除去する関数 //+------------------------------------------------------------------+ /** * @brief Python連携ファイルから読んだ1行をパース前に正規化します。 */ string TrimText(const string value) { string text = value; StringTrimLeft(text); StringTrimRight(text); return text; } //+------------------------------------------------------------------+ //| 10進数字か判定する関数 //+------------------------------------------------------------------+ bool IsDecimalDigit(const int ch) { return (ch >= 48 && ch <= 57); } //+------------------------------------------------------------------+ //| 厳格な整数パース //+------------------------------------------------------------------+ /** * @brief 数字だけで構成された整数行を読み込みます。不正文字があればfalse。 */ bool TryParseStrictInteger(const string value, int &parsed) { string text = TrimText(value); int length = StringLen(text); if(length <= 0) return false; for(int i = 0; i < length; i++) { if(!IsDecimalDigit(StringGetCharacter(text, i))) return false; } parsed = (int)StringToInteger(text); return true; } //+------------------------------------------------------------------+ //| 厳格な小数パース //+------------------------------------------------------------------+ /** * @brief 単純な10進数行を読み込みます。指数表記や余計な文字は許可しません。 */ bool TryParseStrictDouble(const string value, double &parsed) { string text = TrimText(value); int length = StringLen(text); if(length <= 0) return false; bool has_digit = false; bool has_dot = false; int start_index = 0; int first_char = StringGetCharacter(text, 0); if(first_char == 43 || first_char == 45) // + or - { if(length == 1) return false; start_index = 1; } for(int i = start_index; i < length; i++) { int ch = StringGetCharacter(text, i); if(IsDecimalDigit(ch)) { has_digit = true; continue; } if(ch == 46 && !has_dot) // dot { has_dot = true; continue; } return false; } if(!has_digit) return false; parsed = StringToDouble(text); return true; } //+------------------------------------------------------------------+ //| H1候補IDを安全な12桁時刻トークンへ正規化する関数 //+------------------------------------------------------------------+ /** * @brief Pythonから受け取ったcandidate_idが厳格な12桁数字の場合だけ採用します。 */ string NormalizeTargetCandidateId(const string value) { string text = TrimText(value); int length = StringLen(text); if(length != 12) return "0"; for(int i = 0; i < length; i++) { int ch = StringGetCharacter(text, i); if(!IsDecimalDigit(ch)) return "0"; } return text; } //+------------------------------------------------------------------+ //| 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), ")"); 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; } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ /** * @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 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; } 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