diff --git a/Experts/MyProject/HIT-EA_refactor_ver6.mq5 b/Experts/MyProject/HIT-EA_refactor_ver6.mq5 index fb351de..706b7ab 100644 --- a/Experts/MyProject/HIT-EA_refactor_ver6.mq5 +++ b/Experts/MyProject/HIT-EA_refactor_ver6.mq5 @@ -66,12 +66,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"; @@ -264,6 +267,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 +362,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"); diff --git a/Include/MyLib/Common/HITExternalProcess.mqh b/Include/MyLib/Common/HITExternalProcess.mqh index 4ce2b0f..1ee223b 100644 --- a/Include/MyLib/Common/HITExternalProcess.mqh +++ b/Include/MyLib/Common/HITExternalProcess.mqh @@ -308,7 +308,11 @@ 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) { STARTUPINFO_W startup_info = {}; PROCESS_INFORMATION process_info = {}; @@ -316,7 +320,7 @@ 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 + "\"\""; string current_directory = BatchWorkingDirectory(bat_file); int created = CreateProcessW( @@ -353,7 +357,7 @@ 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); return true; } diff --git a/Include/MyLib/Signals/HITEntrySignal.mqh b/Include/MyLib/Signals/HITEntrySignal.mqh index d3f3416..1839f95 100644 --- a/Include/MyLib/Signals/HITEntrySignal.mqh +++ b/Include/MyLib/Signals/HITEntrySignal.mqh @@ -671,7 +671,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 +685,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 +722,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 +736,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; diff --git a/Include/MyLib/Signals/HITPythonSignalGateway.mqh b/Include/MyLib/Signals/HITPythonSignalGateway.mqh index 9b28ad0..bec33a0 100644 --- a/Include/MyLib/Signals/HITPythonSignalGateway.mqh +++ b/Include/MyLib/Signals/HITPythonSignalGateway.mqh @@ -119,6 +119,101 @@ 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; + } + //+------------------------------------------------------------------+ //| バッチファイル(Pythonスクリプト)を実行する関数 //+------------------------------------------------------------------+ @@ -135,7 +230,7 @@ bool ExecuteBatchTrend() return false; DeleteDoneFile(done_trend_file); - return StartBatchProcess(get_trend_reply_bat, running_trend_file, "trend", g_trend_process); + return StartBatchProcess(get_trend_reply_bat, running_trend_file, "trend", g_trend_process, mt5_file_prefix); } //+------------------------------------------------------------------+ @@ -154,7 +249,7 @@ bool ExecuteBatchEntry() return false; DeleteDoneFile(done_entry_file); - return StartBatchProcess(get_entry_reply_bat, running_entry_file, "entry", g_entry_process); + return StartBatchProcess(get_entry_reply_bat, running_entry_file, "entry", g_entry_process, mt5_file_prefix); } @@ -182,7 +277,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 +311,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 +367,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 +421,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 +460,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; // 不完全なファイルは無効扱い } } @@ -411,14 +524,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 +581,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,7 +596,16 @@ 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; } diff --git a/Include/MyLib/Trading/HITTradeManager.mqh b/Include/MyLib/Trading/HITTradeManager.mqh index 65a7d2c..3c85108 100644 --- a/Include/MyLib/Trading/HITTradeManager.mqh +++ b/Include/MyLib/Trading/HITTradeManager.mqh @@ -370,9 +370,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 +387,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 = {}; diff --git a/docs/MyProject/HIT-EA_spec_en.md b/docs/MyProject/HIT-EA_spec_en.md index 2d0f528..63a5dfa 100644 --- a/docs/MyProject/HIT-EA_spec_en.md +++ b/docs/MyProject/HIT-EA_spec_en.md @@ -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__` 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`. + +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: `_ohlc_H4.csv` +- Python output: `_trend_state.txt` +- Done marker: `_process_done_trend.txt` +- Running marker: `_process_running_trend.txt` - Batch file: `\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: `_ohlc_H1.csv` +- Python output: `_target_prices.txt`, `_target_zones.txt` +- Done marker: `_process_done_entry.txt` +- Running marker: `_process_running_entry.txt` - Batch file: `\MQL5\python_for_ea\bat\get_entry_reply.bat` During `OnInit()`, the EA resolves `MQL5\python_for_ea` from `TerminalInfoString(TERMINAL_DATA_PATH)` and builds absolute batch-file paths. Each batch file resolves the Python project root from its own location, so runtime execution no longer depends on `C:\ea_py`. @@ -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. 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: @@ -120,8 +146,9 @@ New entries are allowed only when all conditions below are satisfied: - 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,50 @@ 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. - 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. ## 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 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__` 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. + ### 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 diff --git a/docs/MyProject/HIT-EA_spec_ja.md b/docs/MyProject/HIT-EA_spec_ja.md index 4ae5f70..09f5d77 100644 --- a/docs/MyProject/HIT-EA_spec_ja.md +++ b/docs/MyProject/HIT-EA_spec_ja.md @@ -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__` 形式の接頭辞を作り、MT5 `Files` 配下の連携ファイル名へ付与する。例: `HIT_GOLD_10001_ohlc_H4.csv`。batには同じ接頭辞を第1引数で渡し、bat側が `MT5_EA_FILE_PREFIX` としてPythonへ引き継ぐ。 + +手動実行などで接頭辞が指定されない場合、Pythonは従来どおり `ohlc_H4.csv` などの旧ファイル名を使う。 + ### H4トレンド判定 -- MQL5出力: `ohlc_H4.csv` -- Python出力: `trend_state.txt` -- 完了フラグ: `process_done_trend.txt` -- 実行中フラグ: `process_running_trend.txt` +- MQL5出力: `_ohlc_H4.csv` +- Python出力: `_trend_state.txt` +- 完了フラグ: `_process_done_trend.txt` +- 実行中フラグ: `_process_running_trend.txt` - 起動バッチ: `\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出力: `_ohlc_H1.csv` +- Python出力: `_target_prices.txt`, `_target_zones.txt` +- 完了フラグ: `_process_done_entry.txt` +- 実行中フラグ: `_process_running_entry.txt` - 起動バッチ: `\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` には依存しない。 @@ -142,7 +146,7 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて - 対象注文タイプの `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`: 新規注文を停止。 ### 注文タイプごとの価格整合条件 @@ -189,19 +193,28 @@ Pythonは `target_prices.txt` と `target_zones.txt` の両方を書き終えて - ペンディング注文には、ブローカーが対応する場合にサーバー側の期限を設定する。 - `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を満たさない候補は送信しない。 ## 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プロセスが異常終了した場合、終了コードをログへ出力し、次回トリガーで再実行できる状態に戻す。 - `running` ファイルだけが残っている場合は、PIDからプロセス復元を試みる。復元できずタイムアウト済みなら古いマーカーとして削除する。 ## 10. 変更履歴 +### 2026-05-30 + +- 同一Terminal内の複数EA/複数シンボルでPython連携ファイルが衝突しないよう、`HIT__` 接頭辞を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側距離ガードの既定値を修正した。 + ### 2026-05-27 - Python補助アプリの配置を `C:\ea_py` からMT5データフォルダ配下の `MQL5\python_for_ea` へ移行する仕様に変更した。 @@ -217,8 +230,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 diff --git a/python_for_ea/README.md b/python_for_ea/README.md index e8ecea9..1d7ba33 100644 --- a/python_for_ea/README.md +++ b/python_for_ea/README.md @@ -36,11 +36,13 @@ MT5 EA からは `bat/` 経由でルート直下の互換スクリプトを起 パスの組み立ては `src/ea_py/paths.py` で定義しています。特殊な配置では `MT5_FILES_DIR` または `MT5_DATA_PATH` 環境変数で上書きできます。 +EAからbat経由で起動する場合は、第1引数で渡された `HIT__` 接頭辞が `MT5_EA_FILE_PREFIX` として設定されます。Pythonはこの接頭辞をすべての連携ファイル名へ付けるため、同じTerminal内で複数EAや複数シンボルを動かしてもファイルが衝突しません。手動実行で接頭辞が未設定の場合は、従来のファイル名を使います。 + ### 入力 ```text -ohlc_H4.csv -ohlc_H1.csv +[_]ohlc_H4.csv +[_]ohlc_H1.csv ``` CSV は UTF-8 読み込みで、次の列を必須とします。 @@ -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 +[_]trend_state.txt +[_]target_prices.txt +[_]target_zones.txt +[_]process_done_trend.txt +[_]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` を生成します。 処理の概要: @@ -140,9 +142,10 @@ $env:OPENAI_API_KEY = "..." ```powershell $env:OPENAI_MODEL = "..." $env:OPENAI_REASONING_EFFORT = "low" +$env:MT5_EA_FILE_PREFIX = "HIT_GOLD_10001" ``` -`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` は手動実行時だけ指定すればよく、EA/bat経由では自動設定されます。 ## 主なモジュール @@ -156,6 +159,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 パイプライン diff --git a/python_for_ea/bat/get_entry_reply.bat b/python_for_ea/bat/get_entry_reply.bat index 71fdbeb..852610b 100644 --- a/python_for_ea/bat/get_entry_reply.bat +++ b/python_for_ea/bat/get_entry_reply.bat @@ -5,6 +5,7 @@ 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" for %%I in ("%APP_DIR%") do set "APP_DIR=%%~fI" cd /d "%APP_DIR%" diff --git a/python_for_ea/bat/get_trend_reply.bat b/python_for_ea/bat/get_trend_reply.bat index 8e06ff9..1714276 100644 --- a/python_for_ea/bat/get_trend_reply.bat +++ b/python_for_ea/bat/get_trend_reply.bat @@ -5,6 +5,7 @@ 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" for %%I in ("%APP_DIR%") do set "APP_DIR=%%~fI" cd /d "%APP_DIR%" diff --git a/python_for_ea/docs/architecture/folder-structure.md b/python_for_ea/docs/architecture/folder-structure.md index 7bf0290..09cae5a 100644 --- a/python_for_ea/docs/architecture/folder-structure.md +++ b/python_for_ea/docs/architecture/folder-structure.md @@ -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__` 形式の接頭辞が `MT5_EA_FILE_PREFIX` としてPythonへ渡され、すべての連携ファイル名へ付与される。接頭辞が未設定の手動実行では旧ファイル名を維持する。 + 代表例: ```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呼び出しの薄いラッパー。 | diff --git a/python_for_ea/src/ea_py/paths.py b/python_for_ea/src/ea_py/paths.py index 89c96e9..0555680 100644 --- a/python_for_ea/src/ea_py/paths.py +++ b/python_for_ea/src/ea_py/paths.py @@ -11,6 +11,7 @@ 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" @dataclass(frozen=True) @@ -21,6 +22,7 @@ class Mt5PathSettings: data_path: Path | None = None user_name: str | None = None terminal_id: str | None = None + file_prefix: str | None = None @dataclass(frozen=True) @@ -68,6 +70,39 @@ 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 terminal_files_dir(settings: Mt5PathSettings | None = None) -> Path: """MT5のMQL5/Filesディレクトリを返す。 @@ -127,11 +162,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 +174,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), ) diff --git a/python_for_ea/tests/unit/test_paths.py b/python_for_ea/tests/unit/test_paths.py index cf88647..c8a1e31 100644 --- a/python_for_ea/tests/unit/test_paths.py +++ b/python_for_ea/tests/unit/test_paths.py @@ -7,11 +7,16 @@ from pathlib import Path import pytest from ea_py.paths import ( + ENV_MT5_EA_FILE_PREFIX, ENV_MT5_DATA_PATH, ENV_MT5_FILES_DIR, Mt5PathSettings, + build_entry_paths, + build_trend_paths, discover_mql5_dir, files_dir_from_data_path, + prefixed_file_name, + sanitize_file_prefix, terminal_files_dir, ) @@ -89,3 +94,44 @@ 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"