Strengthen Python timeout recovery and candidate freshness
This commit is contained in:
@@ -36,7 +36,7 @@ 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や複数シンボルを動かしてもファイルが衝突しません。手動実行で接頭辞が未設定の場合は、従来のファイル名を使います。
|
||||
EAからbat経由で起動する場合は、第1引数で渡された `HIT_<symbol>_<magic_number>` 接頭辞が `MT5_EA_FILE_PREFIX` として設定されます。第2引数で渡された `_Symbol` の価格桁数は `MT5_PRICE_DIGITS` として設定されます。Pythonはこの接頭辞をすべての連携ファイル名へ付けるため、同じTerminal内で複数EAや複数シンボルを動かしてもファイルが衝突しません。手動実行で接頭辞が未設定の場合は、従来のファイル名を使います。
|
||||
|
||||
### 入力
|
||||
|
||||
@@ -45,7 +45,7 @@ EAからbat経由で起動する場合は、第1引数で渡された `HIT_<symb
|
||||
[<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
|
||||
@@ -127,6 +127,8 @@ H4 状態ごとの許可戦略:
|
||||
|
||||
EA側で分割エントリーを有効にした場合は、この予測ゾーンを `split_entry_count` 本に分割してpending注文を出します。ロットは総量分割または1注文固定を選択できます。
|
||||
|
||||
`target_zones.txt` の価格小数桁は `MT5_PRICE_DIGITS` に合わせます。EA/bat経由では自動設定され、手動実行で未指定の場合は安全側で5桁を使います。
|
||||
|
||||
## OpenAI 設定
|
||||
|
||||
H1 エントリー候補生成では OpenAI API を使用します。
|
||||
@@ -143,9 +145,10 @@ $env:OPENAI_API_KEY = "..."
|
||||
$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` のいずれかです。`MT5_EA_FILE_PREFIX` は手動実行時だけ指定すればよく、EA/bat経由では自動設定されます。
|
||||
`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経由では自動設定されます。
|
||||
|
||||
## 主なモジュール
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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%"
|
||||
|
||||
@@ -6,6 +6,7 @@ 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%"
|
||||
|
||||
@@ -92,7 +92,7 @@ MT5 EAから `bat/` 経由でルート直下のPythonスクリプトが呼ばれ
|
||||
|
||||
MT5側の `MQL5\Files` 配下に、Pythonとの連携ファイルが置かれる。
|
||||
|
||||
EA/bat経由では `HIT_<symbol>_<magic_number>` 形式の接頭辞が `MT5_EA_FILE_PREFIX` としてPythonへ渡され、すべての連携ファイル名へ付与される。接頭辞が未設定の手動実行では旧ファイル名を維持する。
|
||||
EA/bat経由では `HIT_<symbol>_<magic_number>` 形式の接頭辞が `MT5_EA_FILE_PREFIX` としてPythonへ渡され、すべての連携ファイル名へ付与される。さらに `_Symbol` の価格桁数が `MT5_PRICE_DIGITS` として渡され、`target_zones.txt` の価格出力桁数に使われる。接頭辞が未設定の手動実行では旧ファイル名を維持し、価格桁数が未指定の場合は5桁を使う。
|
||||
|
||||
代表例:
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ 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)
|
||||
@@ -23,6 +24,7 @@ class Mt5PathSettings:
|
||||
user_name: str | None = None
|
||||
terminal_id: str | None = None
|
||||
file_prefix: str | None = None
|
||||
price_digits: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -103,6 +105,30 @@ def prefixed_file_name(base_name: str, settings: Mt5PathSettings | None = None)
|
||||
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ディレクトリを返す。
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
]
|
||||
@@ -10,13 +10,16 @@ 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,
|
||||
)
|
||||
|
||||
@@ -135,3 +138,25 @@ def test_build_paths_apply_file_prefix_from_environment(
|
||||
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