Add file-prefix isolation and strict split-order parsing

This commit is contained in:
Hiroaki86
2026-05-30 00:56:18 +09:00
parent 3d853fa67a
commit 265d378e07
13 changed files with 513 additions and 116 deletions
+13 -9
View File
@@ -36,11 +36,13 @@ 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` として設定されます。Pythonはこの接頭辞をすべての連携ファイル名へ付けるため、同じTerminal内で複数EAや複数シンボルを動かしてもファイルが衝突しません。手動実行で接頭辞が未設定の場合は、従来のファイル名を使います。
### 入力
```text
ohlc_H4.csv
ohlc_H1.csv
[<prefix>_]ohlc_H4.csv
[<prefix>_]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
[<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` を生成します。
処理の概要:
@@ -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 パイプライン
+1
View File
@@ -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%"
+1
View File
@@ -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%"
@@ -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へ渡され、すべての連携ファイル名へ付与される。接頭辞が未設定の手動実行では旧ファイル名を維持する。
代表例:
```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呼び出しの薄いラッパー。 |
+48 -13
View File
@@ -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),
)
+46
View File
@@ -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"