Consolidate Python ignore rules into root gitignore

This commit is contained in:
Hiroaki86
2026-05-27 23:01:28 +09:00
commit fa3394415d
399 changed files with 509103 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
"""HIT-EA Python helper package."""
@@ -0,0 +1,2 @@
"""Chart rendering helpers."""
@@ -0,0 +1,81 @@
"""ローソク足チャート画像の生成とdata URL変換。"""
from __future__ import annotations
import base64
from pathlib import Path
from typing import Sequence
import matplotlib.pyplot as plt
from ea_py.types import OhlcBar
def ohlc_to_candlestick_png_file(
*,
ohlc_data: Sequence[OhlcBar],
save_path: Path,
instrument: str = "GOLD",
timeframe: str = "H4",
dark: bool = True,
dpi: int = 180,
figsize: tuple[int, int] = (12, 4),
) -> None:
"""OHLCバー配列をローソク足PNGとして保存する。"""
if not ohlc_data:
raise ValueError("ohlc_data is empty")
if dark:
plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
ax.grid(True, linestyle=":", linewidth=0.6, alpha=0.6)
candle_w = 0.55
wick_lw = 1.0
for index, bar in enumerate(ohlc_data):
open_price = bar["Open"]
high_price = bar["High"]
low_price = bar["Low"]
close_price = bar["Close"]
is_up = close_price >= open_price
color = "#00ff66" if is_up else "#ff3355"
ax.vlines(index, low_price, high_price, linewidth=wick_lw, color=color)
body_low = min(open_price, close_price)
body_height = max(abs(close_price - open_price), 1e-8)
rect = plt.Rectangle(
(index - candle_w / 2, body_low),
candle_w,
body_height,
color=color,
alpha=0.95,
)
ax.add_patch(rect)
ax.yaxis.tick_right()
ax.yaxis.set_label_position("right")
dt_labels = [bar["DateTime"] for bar in ohlc_data]
candle_count = len(ohlc_data)
show_idx = [0, candle_count // 2, candle_count - 1] if candle_count >= 3 else list(range(candle_count))
ax.set_xticks(show_idx)
ax.set_xticklabels([dt_labels[i] for i in show_idx], fontsize=8)
ax.set_xlim(-1, candle_count)
ax.set_title(f"{instrument} {timeframe} ({candle_count} candles)", fontsize=10)
plt.tight_layout()
fig.savefig(save_path, format="png")
plt.close(fig)
def png_file_to_data_url(path: Path) -> str:
"""PNGファイルをOpenAI Vision入力用のdata URLへ変換する。"""
encoded = base64.b64encode(path.read_bytes()).decode("utf-8")
return f"data:image/png;base64,{encoded}"
+56
View File
@@ -0,0 +1,56 @@
"""環境変数から実行時設定を読み込む。"""
from __future__ import annotations
from dataclasses import dataclass
import os
from ea_py.constants import DEBUG_PRINT, DEFAULT_GPT_MODEL, DEFAULT_REASONING_EFFORT, VALID_REASONING_EFFORTS
@dataclass(frozen=True)
class RuntimeConfig:
"""OpenAI呼び出しに必要な実行時設定。"""
api_key: str
model: str
reasoning_effort: str
debug_print: bool
def load_runtime_config(
*,
model: str | None = None,
reasoning_effort: str | None = None,
debug_print: bool = DEBUG_PRINT,
) -> RuntimeConfig:
"""OpenAI呼び出しに必要な実行時設定を読み込む。
APIキーは秘密情報のためコードや設定ファイルへ直書きせず、
`OPENAI_API_KEY` 環境変数からのみ取得する。
未設定の場合は `RuntimeError` を送出し、上位パイプラインで停止値を出力する。
`model` / `reasoning_effort` / `debug_print` は呼び出し側から上書き可能だが、
通常は `constants.py` のデフォルト値を使う。
`OPENAI_MODEL` と `OPENAI_REASONING_EFFORT` が設定されている場合は、
引数未指定時の実行時上書きとして扱う。
"""
api_key = os.getenv("OPENAI_API_KEY")
if api_key is None:
raise RuntimeError("OPENAI_API_KEY が環境変数に設定されていません。")
selected_model = model or os.getenv("OPENAI_MODEL") or DEFAULT_GPT_MODEL
selected_reasoning_effort = reasoning_effort or os.getenv("OPENAI_REASONING_EFFORT") or DEFAULT_REASONING_EFFORT
if selected_reasoning_effort not in VALID_REASONING_EFFORTS:
allowed = ", ".join(sorted(VALID_REASONING_EFFORTS))
raise RuntimeError(
f"OPENAI_REASONING_EFFORT が不正です: {selected_reasoning_effort!r}. "
f"allowed={allowed}"
)
return RuntimeConfig(
api_key=api_key,
model=selected_model,
reasoning_effort=selected_reasoning_effort,
debug_print=debug_print,
)
+54
View File
@@ -0,0 +1,54 @@
"""HIT-EA連携処理で共有する定数。"""
from __future__ import annotations
MT_ENCODING = "utf-16 LE"
DEFAULT_GPT_MODEL = "gpt-5.5-2026-04-23"
DEFAULT_REASONING_EFFORT = "low"
VALID_REASONING_EFFORTS = frozenset({"none", "low", "medium", "high", "xhigh"})
DEBUG_PRINT = True
ATR_PERIOD = 14
ATR_BASELINE_PERIOD = 20
HIGH_VOL_MULTIPLIER = 1.20
ABNORMAL_VOL_MULTIPLIER = 2.00
ABNORMAL_RANGE_MULTIPLIER = 2.50
MIN_VOL_BARS = ATR_PERIOD + 1
USE_IMBALANCE_FILTER = True
IMBALANCE_AVG_BODY_PERIOD = 20
IMBALANCE_SENSITIVITY = 2.5
IMBALANCE_MIN_AVG_BODY_SIZE = 0.01
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER = 1.00
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER = 1.50
ENTRY_MAX_DISTANCE_MIN_PRICE = 5.0
CANDLE_TREND = 72
CANDLE_SHORT = 36
CANDLE_LONG = 72
INSTRUMENT = "GOLD"
TREND_TIMEFRAME = "H4"
ENTRY_TIMEFRAME = "H1"
TARGET_SIZE = 13
TECHNICAL_ERROR_STOP = 6
TREND_MAX_OUTPUT_TOKENS = 128
TREND_DEBUG_MAX_OUTPUT_TOKENS = 512
ENTRY_MAX_OUTPUT_TOKENS = 512
ENTRY_DEBUG_MAX_OUTPUT_TOKENS = 1200
MARKET_STATE_LABELS: dict[int, str] = {
0: "LOW_VOL_RANGE",
1: "HIGH_VOL_RANGE",
2: "LOW_VOL_UP",
3: "HIGH_VOL_UP",
4: "LOW_VOL_DOWN",
5: "HIGH_VOL_DOWN",
6: "TECHNICAL_ERROR_STOP",
}
VALID_MARKET_STATES = frozenset(MARKET_STATE_LABELS)
+2
View File
@@ -0,0 +1,2 @@
"""I/O helpers for MT5 integration files."""
+105
View File
@@ -0,0 +1,105 @@
"""デバッグ理由ログをMT5 Files配下へ追記する。"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import Sequence
from ea_py.constants import MARKET_STATE_LABELS
def now_str() -> str:
"""デバッグログ用の現在時刻文字列を返す。"""
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def append_debug_trend(
*,
path: Path,
model: str,
reasoning_effort: str,
max_output_tokens: int,
api_diagnostics: str,
current_price: float,
numeric_summary: str,
direction_numeric: str,
market_state: int,
classification_reason: str,
reason_text: str,
) -> None:
"""H4トレンド判定の数値要約、方向出力、分類理由を追記する。"""
with path.open("a", encoding="utf-8") as file:
file.write("=" * 60 + "\n")
file.write(f"DEBUG TIME : {now_str()}\n")
file.write(f"MODEL : {model}\n")
file.write(f"REASONING : {reasoning_effort}\n")
file.write(f"MAX TOKENS : {max_output_tokens}\n")
file.write(f"CURRENT PRICE : {current_price:.2f}\n")
file.write(f"MARKET STATE : {market_state} ({MARKET_STATE_LABELS.get(market_state, 'UNKNOWN')})\n")
file.write("=" * 60 + "\n\n")
file.write("---- API DIAGNOSTICS START ----\n")
file.write((api_diagnostics or "").strip() + "\n")
file.write("---- API DIAGNOSTICS END ----\n\n")
file.write("---- NUMERIC SUMMARY START ----\n")
file.write((numeric_summary or "").strip() + "\n")
file.write("---- NUMERIC SUMMARY END ----\n\n")
file.write("---- DIRECTION NUMERIC (0/1/2) START ----\n")
file.write((direction_numeric or "").strip() + "\n")
file.write("---- DIRECTION NUMERIC (0/1/2) END ----\n\n")
file.write("---- MARKET STATE CLASSIFICATION START ----\n")
file.write((classification_reason or "").strip() + "\n")
file.write("---- MARKET STATE CLASSIFICATION END ----\n\n")
file.write("---- REASON START ----\n")
file.write((reason_text or "").strip() + "\n")
file.write("---- REASON END ----\n\n")
def append_debug_entry(
*,
path: Path,
model: str,
reasoning_effort: str,
max_output_tokens: int,
api_diagnostics: str,
timeframe: str,
current_price: float,
trend_state: int,
selected_strategies: Sequence[int],
imbalance_summary: str,
numeric_summary: str,
numeric_lines: str,
post_filter_summary: str,
sanitized_numeric_list: Sequence[int | float] | None,
reason_text: str,
) -> None:
"""H1エントリー候補生成の数値行と理由を追記する。"""
with path.open("a", encoding="utf-8") as file:
file.write("=" * 60 + "\n")
file.write(f"DEBUG TIME : {now_str()}\n")
file.write(f"MODEL : {model}\n")
file.write(f"REASONING : {reasoning_effort}\n")
file.write(f"MAX TOKENS : {max_output_tokens}\n")
file.write(f"TIMEFRAME : {timeframe}\n")
file.write(f"MARKET_STATE(H4) : {trend_state} ({MARKET_STATE_LABELS.get(trend_state, 'UNKNOWN')})\n")
file.write(f"SELECTED_STRATEGY : {','.join(str(x) for x in selected_strategies)}\n")
file.write(f"IMBALANCE(H1) : {imbalance_summary}\n")
file.write(f"CURRENT PRICE : {current_price:.2f}\n")
file.write("=" * 60 + "\n\n")
file.write("---- API DIAGNOSTICS START ----\n")
file.write((api_diagnostics or "").strip() + "\n")
file.write("---- API DIAGNOSTICS END ----\n\n")
file.write("---- NUMERIC SUMMARY START ----\n")
file.write((numeric_summary or "").strip() + "\n")
file.write("---- NUMERIC SUMMARY END ----\n\n")
file.write("---- GPT NUMERIC LINES START ----\n")
file.write((numeric_lines or "").strip() + "\n")
file.write("---- GPT NUMERIC LINES END ----\n\n")
file.write("---- POST FILTER START ----\n")
file.write((post_filter_summary or "").strip() + "\n")
if sanitized_numeric_list is not None:
file.write("sanitized_numeric_list=" + ",".join(str(x) for x in sanitized_numeric_list) + "\n")
file.write("---- POST FILTER END ----\n\n")
file.write("---- REASON START ----\n")
file.write((reason_text or "").strip() + "\n")
file.write("---- REASON END ----\n\n")
+66
View File
@@ -0,0 +1,66 @@
"""MT5連携ファイルの読み書き補助。"""
from __future__ import annotations
from pathlib import Path
from typing import Collection
def write_text_atomic(path: Path, text: str, encoding: str) -> None:
"""一時ファイルへ書いてから対象ファイルへ置換する。"""
tmp_path = path.with_name(f"{path.name}.tmp")
tmp_path.write_text(text, encoding=encoding, newline="")
tmp_path.replace(path)
def write_result_then_done(
*,
result_path: Path,
result_text: str,
result_encoding: str,
done_path: Path,
) -> None:
"""MT5へ結果ファイルの完成を通知する順序で書き込む。
手順は必ず以下の順序にする。
1. 古いdoneファイルを削除する。
2. 結果ファイルを一時ファイルへ書き、atomic replaceで本番名へ置換する。
3. 結果ファイルが完成した後にdoneファイルを作成する。
MT5 EAはdoneファイルを完了シグナルとして扱うため、古いdoneが残った状態で
新しい結果を書き始めないことが重要。結果書き込みに失敗した場合はdoneを作らず、
EA側が未完成の結果を読むリスクを避ける。
"""
done_path.unlink(missing_ok=True)
write_text_atomic(result_path, result_text, result_encoding)
write_text_atomic(done_path, "", "utf-8")
def write_results_then_done(
*,
result_files: Collection[tuple[Path, str, str]],
done_path: Path,
) -> None:
"""複数の結果ファイルを書き終えてからdoneファイルを作成する。"""
done_path.unlink(missing_ok=True)
for result_path, result_text, result_encoding in result_files:
write_text_atomic(result_path, result_text, result_encoding)
write_text_atomic(done_path, "", "utf-8")
def read_int_file(
path: Path,
*,
encoding: str,
allowed_values: Collection[int],
default: int,
) -> int:
"""整数ファイルを読み、許可値以外や失敗時はdefaultを返す。"""
try:
if not path.exists():
return default
value = int(path.read_text(encoding=encoding).strip())
return value if value in allowed_values else default
except Exception:
return default
+48
View File
@@ -0,0 +1,48 @@
"""MT5が出力したOHLC CSVを読み込む。"""
from __future__ import annotations
from pathlib import Path
import pandas as pd
from ea_py.types import OhlcBar
REQUIRED_COLUMNS = ("Time", "Open", "High", "Low", "Close")
def read_ohlc_csv(path: Path) -> list[OhlcBar]:
"""MT5が出力したOHLC CSVを検証してOhlcBar配列へ変換する。
入力CSVは `Time,Open,High,Low,Close` 列を必須とする。
`Time` は文字列、価格列はfloatへ変換し、内部表現では既存プロンプトと
チャート生成処理に合わせて `Time` を `DateTime` キーへ写す。
必須列が欠けている場合や価格列のfloat変換に失敗した場合は例外を送出する。
上位パイプラインはその例外を捕捉し、MT5へ停止値を返す。
"""
df = pd.read_csv(path, encoding="utf-8")
missing_columns = [column for column in REQUIRED_COLUMNS if column not in df.columns]
if missing_columns:
joined = ", ".join(missing_columns)
raise ValueError(f"OHLC CSV missing column(s): {joined}")
df["Time"] = df["Time"].astype(str)
df["Open"] = df["Open"].astype(float)
df["High"] = df["High"].astype(float)
df["Low"] = df["Low"].astype(float)
df["Close"] = df["Close"].astype(float)
ohlc: list[OhlcBar] = []
for _, row in df.iterrows():
ohlc.append(
{
"DateTime": row["Time"],
"Open": float(row["Open"]),
"High": float(row["High"]),
"Low": float(row["Low"]),
"Close": float(row["Close"]),
}
)
return ohlc
+130
View File
@@ -0,0 +1,130 @@
"""OpenAI Responses API呼び出しの薄いラッパー。"""
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
import json
import logging
from typing import Any
from openai import OpenAI
from openai.types.responses import (
EasyInputMessageParam,
ResponseInputImageParam,
ResponseInputMessageContentListParam,
ResponseInputParam,
ResponseInputTextParam,
)
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class ResponsesApiDiagnostics:
"""Responses APIの空出力診断に必要な最小メタデータ。"""
model: str
status: str
incomplete_details: str
error: str
usage: str
def to_log_text(self) -> str:
"""デバッグログへそのまま書ける1行診断文字列を返す。"""
parts = [
f"model={self.model or '-'}",
f"status={self.status or '-'}",
f"incomplete_details={self.incomplete_details or '-'}",
f"error={self.error or '-'}",
f"usage={self.usage or '-'}",
]
return "; ".join(parts)
@dataclass(frozen=True)
class ResponsesApiResult:
"""Responses APIの本文と診断情報。"""
text: str
diagnostics: ResponsesApiDiagnostics
def create_openai_client(api_key: str) -> OpenAI:
"""APIキーからOpenAIクライアントを生成する。"""
return OpenAI(api_key=api_key)
def _format_response_field(value: object) -> str:
"""SDKオブジェクトを短いJSON文字列へ変換する。"""
if value is None:
return ""
try:
if hasattr(value, "model_dump"):
dumped = value.model_dump(mode="json", exclude_none=True) # type: ignore[attr-defined]
return json.dumps(dumped, ensure_ascii=False, separators=(",", ":"))
return str(value)
except Exception:
return repr(value)
def _extract_response_diagnostics(response: object) -> ResponsesApiDiagnostics:
"""Responses APIレスポンスから安全な診断情報を抽出する。"""
return ResponsesApiDiagnostics(
model=str(getattr(response, "model", "") or ""),
status=str(getattr(response, "status", "") or ""),
incomplete_details=_format_response_field(getattr(response, "incomplete_details", None)),
error=_format_response_field(getattr(response, "error", None)),
usage=_format_response_field(getattr(response, "usage", None)),
)
def call_responses_api(
*,
client: OpenAI,
model: str,
reasoning_effort: str,
system_content: str,
user_text: str,
image_data_urls: Sequence[str],
max_output_tokens: int,
) -> ResponsesApiResult:
"""Responses APIへテキストとチャート画像を送り、出力テキストを返す。
`system_content` は判定器としての役割と出力制約を指定する。
`user_text` はH4/H1の具体的な分析依頼、数値要約、出力フォーマットを含む。
`image_data_urls` にはPNGをdata URL化したチャート画像を渡す。空文字は無視する。
戻り値は `response.output_text` をstripした文字列とAPI診断情報。
API例外や空/不正な出力の安全側処理は、この薄いラッパーではなく
呼び出し元のパイプラインとパース関数が担当する。
"""
text_part: ResponseInputTextParam = {"type": "input_text", "text": user_text}
content_parts: ResponseInputMessageContentListParam = [text_part]
for image_data_url in image_data_urls:
if image_data_url:
image_part: ResponseInputImageParam = {
"type": "input_image",
"image_url": image_data_url,
"detail": "auto",
}
content_parts.append(image_part)
system_message: EasyInputMessageParam = {"role": "system", "content": system_content}
user_message: EasyInputMessageParam = {"role": "user", "content": content_parts}
input_messages: ResponseInputParam = [system_message, user_message]
create_params: dict[str, Any] = {
"model": model,
"input": input_messages,
"reasoning": {"effort": reasoning_effort},
"max_output_tokens": max_output_tokens,
}
response = client.responses.create(**create_params)
diagnostics = _extract_response_diagnostics(response)
text = (response.output_text or "").strip()
if not text:
logger.warning("OpenAI response output_text is empty: %s", diagnostics.to_log_text())
return ResponsesApiResult(text=text, diagnostics=diagnostics)
+86
View File
@@ -0,0 +1,86 @@
"""MT5連携ファイルのパスを組み立てる。"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
DEFAULT_USER_NAME = "new"
DEFAULT_TERMINAL_ID = "5BDB0B60344C088C2FA5CA35699BAAFD"
@dataclass(frozen=True)
class Mt5PathSettings:
"""MT5データフォルダを特定するための設定。"""
user_name: str = DEFAULT_USER_NAME
terminal_id: str = DEFAULT_TERMINAL_ID
@dataclass(frozen=True)
class TrendPaths:
"""H4トレンド判定パイプラインが使用するファイル群。"""
input_csv: Path
trend_state: Path
done_trend: Path
tmp_chart: Path
debug_reason: Path
@dataclass(frozen=True)
class EntryPaths:
"""H1エントリー候補生成パイプラインが使用するファイル群。"""
input_csv: Path
output_prices: Path
output_zones: Path
trend_state: Path
done_entry: Path
tmp_short_chart: Path
tmp_long_chart: Path
debug_reason: Path
def terminal_files_dir(settings: Mt5PathSettings | None = None) -> Path:
"""MT5のMQL5/Filesディレクトリを返す。"""
mt5_settings = settings or Mt5PathSettings()
return (
Path("C:/Users")
/ mt5_settings.user_name
/ "AppData"
/ "Roaming"
/ "MetaQuotes"
/ "Terminal"
/ mt5_settings.terminal_id
/ "MQL5"
/ "Files"
)
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",
)
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",
)
@@ -0,0 +1,2 @@
"""Executable pipeline orchestration."""
@@ -0,0 +1,317 @@
"""H1 OHLCとH4 market_stateからtarget_pricesを生成するパイプライン。"""
from __future__ import annotations
import logging
from collections.abc import Sequence
from ea_py.charting.candlestick import ohlc_to_candlestick_png_file, png_file_to_data_url
from ea_py.config import load_runtime_config
from ea_py.constants import (
CANDLE_LONG,
CANDLE_SHORT,
DEBUG_PRINT,
ENTRY_DEBUG_MAX_OUTPUT_TOKENS,
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER,
ENTRY_MAX_DISTANCE_MIN_PRICE,
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER,
ENTRY_MAX_OUTPUT_TOKENS,
ENTRY_TIMEFRAME,
IMBALANCE_AVG_BODY_PERIOD,
IMBALANCE_MIN_AVG_BODY_SIZE,
IMBALANCE_SENSITIVITY,
INSTRUMENT,
MARKET_STATE_LABELS,
MT_ENCODING,
TECHNICAL_ERROR_STOP,
USE_IMBALANCE_FILTER,
)
from ea_py.io.debug_logs import append_debug_entry
from ea_py.io.mt5_files import read_int_file, write_results_then_done
from ea_py.io.ohlc_csv import read_ohlc_csv
from ea_py.market.volatility import summarize_ohlc
from ea_py.market.target_prices import (
extract_entry_blocks_debug,
stop_numeric_list,
strategies_by_trend,
)
from ea_py.market.target_zones import (
build_candidate_id,
format_target_zones,
parse_lines_to_entry_zones_allow_subset,
sanitize_entry_zones,
stop_entry_zones,
zones_to_numeric_list,
)
from ea_py.market.imbalance import (
adjust_strategies_for_imbalance,
detect_imbalance_signal,
format_imbalance_summary,
)
from ea_py.openai_client import call_responses_api, create_openai_client
from ea_py.paths import build_entry_paths
from ea_py.prompts.entry_prompt import (
build_imbalance_guidance,
build_caution_block,
build_common_rules_block,
build_common_rules_block_debug,
build_header,
build_market_state_guidance,
build_numeric_summary,
build_system_content_block,
build_system_content_block_debug,
)
from ea_py.types import NumericList
logger = logging.getLogger(__name__)
TREND_STOP_STRATEGIES = frozenset({1, 3})
def write_entry_output(numeric_list: NumericList, zone_text: str | None = None) -> None:
"""target_prices.txtとtarget_zones.txtを書き、完了後にprocess_done_entry.txtを作る。"""
paths = build_entry_paths()
content = "".join(f"{number}\n" for number in numeric_list)
zone_content = zone_text if zone_text is not None else format_target_zones(stop_entry_zones(), "0")
write_results_then_done(
result_files=[
(paths.output_prices, content, MT_ENCODING),
(paths.output_zones, zone_content, MT_ENCODING),
],
done_path=paths.done_entry,
)
def build_entry_distance_limits(h1_eatr: float, selected_strategies: Sequence[int]) -> dict[int, float]:
"""H1 EATRから戦略別のエントリー許容距離を作る。"""
limits: dict[int, float] = {}
for strategy in selected_strategies:
multiplier = (
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER
if strategy in TREND_STOP_STRATEGIES
else ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER
)
limits[strategy] = max(h1_eatr * multiplier, ENTRY_MAX_DISTANCE_MIN_PRICE)
return limits
def run_pipeline() -> None:
"""H1 OHLCとH4 market_stateからMT5向けtarget_pricesを生成する。
実行契約:
- `trend_state.txt` のH4 `market_state` を読み、許可戦略だけを選ぶ。
- `ohlc_H1.csv` から短期・中期チャート画像と数値要約を作る。
- OpenAIには選択済み戦略の候補価格だけを依頼し、返却行を13値形式へ展開する。
- GPT出力は `sanitize_numeric_list` で検証し、`target_prices.txt` を書いた後に
`process_done_entry.txt` を作成する。
安全側フォールバック:
H4が技術エラー停止値、CSV読込失敗、H1データ不足、画像生成失敗、OpenAI設定/API失敗、
GPT出力不正、または全候補が価格条件違反の場合は13値すべてを停止値にする。
Pythonは候補価格を作るだけで、注文送信やポジション操作は行わない。
"""
paths = build_entry_paths()
trend_state = read_int_file(
paths.trend_state,
encoding=MT_ENCODING,
allowed_values=MARKET_STATE_LABELS.keys(),
default=TECHNICAL_ERROR_STOP,
)
selected_strategies = strategies_by_trend(trend_state)
if trend_state == TECHNICAL_ERROR_STOP or not selected_strategies:
logger.info(
"market_state=%s (%s). New entries stopped.",
trend_state,
MARKET_STATE_LABELS.get(trend_state, "UNKNOWN"),
)
write_entry_output(stop_numeric_list())
return
try:
ohlc_all = read_ohlc_csv(paths.input_csv)
except Exception:
logger.exception("CSV 読み込みエラー")
write_entry_output(stop_numeric_list())
return
if len(ohlc_all) < CANDLE_LONG:
logger.error("データ本数不足: len(ohlc)=%s (need >= %s)", len(ohlc_all), CANDLE_LONG)
write_entry_output(stop_numeric_list())
return
ohlc_short = ohlc_all[-CANDLE_SHORT:]
ohlc_long = ohlc_all[-CANDLE_LONG:]
current_price = float(ohlc_short[-1]["Close"])
candidate_id = build_candidate_id(str(ohlc_short[-1].get("DateTime", "")))
imbalance_analysis = detect_imbalance_signal(
ohlc_all,
avg_body_period=IMBALANCE_AVG_BODY_PERIOD,
sensitivity=IMBALANCE_SENSITIVITY,
min_avg_body_size=IMBALANCE_MIN_AVG_BODY_SIZE,
)
imbalance_summary = format_imbalance_summary(imbalance_analysis)
selected_strategies = adjust_strategies_for_imbalance(
selected_strategies,
trend_state=trend_state,
analysis=imbalance_analysis,
use_filter=USE_IMBALANCE_FILTER,
)
if not selected_strategies:
logger.info(
"H1 imbalance conflicts with market_state=%s (%s). New entries stopped. %s",
trend_state,
MARKET_STATE_LABELS.get(trend_state, "UNKNOWN"),
imbalance_summary,
)
write_entry_output(stop_numeric_list())
return
try:
ohlc_to_candlestick_png_file(
ohlc_data=ohlc_short,
save_path=paths.tmp_short_chart,
instrument=INSTRUMENT,
timeframe=ENTRY_TIMEFRAME,
dark=True,
)
ohlc_to_candlestick_png_file(
ohlc_data=ohlc_long,
save_path=paths.tmp_long_chart,
instrument=INSTRUMENT,
timeframe=ENTRY_TIMEFRAME,
dark=True,
)
images_data_urls = [
png_file_to_data_url(paths.tmp_short_chart),
png_file_to_data_url(paths.tmp_long_chart),
]
except Exception:
logger.exception("画像生成エラー")
write_entry_output(stop_numeric_list())
return
numeric_summary = build_numeric_summary(
current_price=current_price,
ohlc_short=ohlc_short,
ohlc_long=ohlc_long,
)
short_summary = summarize_ohlc(ohlc_short)
h1_eatr = float(short_summary.get("eatr", 0.0))
max_entry_distance = build_entry_distance_limits(h1_eatr, selected_strategies)
post_filter_summary = (
"entry_distance_guard="
f"max_distance_by_strategy={max_entry_distance}, "
f"h1_eatr={h1_eatr:.2f}, "
f"stop_multiplier={ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER:.2f}, "
f"limit_multiplier={ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER:.2f}, "
f"floor={ENTRY_MAX_DISTANCE_MIN_PRICE:.2f}"
)
header = build_header(
current_price=current_price,
numeric_summary=numeric_summary,
trend_state=trend_state,
)
market_state_guidance = build_market_state_guidance(trend_state)
imbalance_guidance = build_imbalance_guidance(imbalance_analysis)
caution = build_caution_block()
try:
config = load_runtime_config(debug_print=DEBUG_PRINT)
except RuntimeError:
logger.exception("OpenAI 設定エラー")
write_entry_output(stop_numeric_list())
return
if config.debug_print:
system_content = build_system_content_block_debug()
common_rules = build_common_rules_block_debug(selected_strategies, max_entry_distance)
max_tokens = ENTRY_DEBUG_MAX_OUTPUT_TOKENS
else:
system_content = build_system_content_block()
common_rules = build_common_rules_block(selected_strategies, max_entry_distance)
max_tokens = ENTRY_MAX_OUTPUT_TOKENS
user_text = "\n\n".join([header, market_state_guidance, imbalance_guidance, common_rules, caution]).strip()
api_diagnostics = ""
try:
client = create_openai_client(config.api_key)
gpt_result = call_responses_api(
client=client,
model=config.model,
reasoning_effort=config.reasoning_effort,
system_content=system_content,
user_text=user_text,
image_data_urls=images_data_urls,
max_output_tokens=max_tokens,
)
gpt_reply = gpt_result.text
api_diagnostics = gpt_result.diagnostics.to_log_text()
except Exception:
logger.exception("OpenAI APIエラー")
write_entry_output(stop_numeric_list())
return
if config.debug_print:
logger.info(
"---- TREND ---- %s selected=%s\n---- GPT REPLY START ----\n%s\n---- GPT REPLY END ----",
trend_state,
selected_strategies,
gpt_reply,
)
if config.debug_print:
numeric_lines, reason_text = extract_entry_blocks_debug(gpt_reply)
if not numeric_lines:
numeric_lines = gpt_reply
entry_zones = parse_lines_to_entry_zones_allow_subset(numeric_lines)
entry_zones = sanitize_entry_zones(
entry_zones,
selected_strategies,
current_price,
max_entry_distance=max_entry_distance,
)
numeric_list = zones_to_numeric_list(entry_zones)
zone_text = format_target_zones(entry_zones, candidate_id)
try:
append_debug_entry(
path=paths.debug_reason,
model=config.model,
reasoning_effort=config.reasoning_effort,
max_output_tokens=max_tokens,
api_diagnostics=api_diagnostics,
timeframe=ENTRY_TIMEFRAME,
current_price=current_price,
trend_state=trend_state,
selected_strategies=selected_strategies,
imbalance_summary=imbalance_summary,
numeric_summary=numeric_summary,
numeric_lines=numeric_lines,
post_filter_summary=post_filter_summary,
sanitized_numeric_list=numeric_list,
reason_text=f"{reason_text}\n\nTARGET_ZONES\n{zone_text}".strip(),
)
except Exception:
logger.exception("debug_entry.txt write error")
else:
entry_zones = parse_lines_to_entry_zones_allow_subset(gpt_reply)
entry_zones = sanitize_entry_zones(
entry_zones,
selected_strategies,
current_price,
max_entry_distance=max_entry_distance,
)
numeric_list = zones_to_numeric_list(entry_zones)
zone_text = format_target_zones(entry_zones, candidate_id)
write_entry_output(numeric_list, zone_text)
def main() -> None:
"""ログ設定を行ってH1エントリー候補生成パイプラインを起動する。"""
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s")
run_pipeline()
@@ -0,0 +1,104 @@
"""H4 OHLCからmarket_stateを生成するパイプライン。"""
from __future__ import annotations
import logging
from ea_py.constants import (
CANDLE_TREND,
DEBUG_PRINT,
MIN_VOL_BARS,
MT_ENCODING,
TECHNICAL_ERROR_STOP,
)
from ea_py.io.debug_logs import append_debug_trend
from ea_py.io.mt5_files import write_result_then_done
from ea_py.io.ohlc_csv import read_ohlc_csv
from ea_py.market.trend_state import (
classify_direction_from_ohlc,
classify_market_state,
)
from ea_py.market.volatility import summarize_ohlc
from ea_py.paths import build_trend_paths
from ea_py.prompts.trend_prompt import build_trend_numeric_summary
logger = logging.getLogger(__name__)
def write_trend_output(market_state: int) -> None:
"""trend_state.txtを書き、完了後にprocess_done_trend.txtを作る。"""
paths = build_trend_paths()
write_result_then_done(
result_path=paths.trend_state,
result_text=str(market_state),
result_encoding=MT_ENCODING,
done_path=paths.done_trend,
)
def run_pipeline() -> None:
"""H4 OHLCからMT5向けのmarket_stateを生成する。
実行契約:
- `ohlc_H4.csv` を読み、直近H4足を数値要約へ変換する。
- EMA、レンジ内位置、高安更新、DMI系方向優位、効率比から
H4方向0/1/2を判定する。
- 方向0/1/2をEATRベースのボラティリティ分類と合成して
`market_state` 0..6 を決定する。
- 最後に `trend_state.txt` をMT5用エンコーディングで書き、
書き込み完了後に `process_done_trend.txt` を作成する。
安全側フォールバック:
CSV読込失敗、データ不足、EATR異常値は
技術エラー停止値として6を出力し、H1側で新規注文を抑止する。
相場ボラティリティが旧異常閾値を超えた場合は、停止せず高ボラstateへ吸収する。
"""
paths = build_trend_paths()
market_state = TECHNICAL_ERROR_STOP
try:
ohlc_all = read_ohlc_csv(paths.input_csv)
except Exception:
logger.exception("CSV 読み込みエラー")
write_trend_output(market_state)
return
if len(ohlc_all) < MIN_VOL_BARS:
logger.error("データ本数不足: len(ohlc)=%s (need >= %s)", len(ohlc_all), MIN_VOL_BARS)
write_trend_output(market_state)
return
ohlc_trend = ohlc_all[-min(CANDLE_TREND, len(ohlc_all)) :]
current_price = float(ohlc_trend[-1]["Close"])
summary = summarize_ohlc(ohlc_trend)
numeric_summary = build_trend_numeric_summary(current_price=current_price, ohlc_trend=ohlc_trend)
direction_val, direction_reason = classify_direction_from_ohlc(ohlc_trend, summary)
market_state, market_state_reason = classify_market_state(direction_val, summary)
classification_reason = f"{direction_reason}\n{market_state_reason}"
if DEBUG_PRINT:
try:
append_debug_trend(
path=paths.debug_reason,
model="rule-based-h4-direction",
reasoning_effort="none",
max_output_tokens=0,
api_diagnostics="OpenAI trend direction call is not used.",
current_price=current_price,
numeric_summary=numeric_summary,
direction_numeric=str(direction_val),
market_state=market_state,
classification_reason=classification_reason,
reason_text=direction_reason,
)
except Exception:
logger.exception("debug_trend.txt write error")
write_trend_output(market_state)
def main() -> None:
"""ログ設定を行ってH4トレンド判定パイプラインを起動する。"""
logging.basicConfig(level=logging.INFO, format="%(levelname)s:%(name)s:%(message)s")
run_pipeline()
@@ -0,0 +1,2 @@
"""Prompt builders for OpenAI calls."""
@@ -0,0 +1,323 @@
"""H1エントリー候補生成用のプロンプトを生成する。"""
from __future__ import annotations
from collections.abc import Sequence
from ea_py.constants import CANDLE_LONG, CANDLE_SHORT, ENTRY_TIMEFRAME, MARKET_STATE_LABELS
from ea_py.market.imbalance import ImbalanceAnalysis, format_imbalance_summary
from ea_py.market.volatility import summarize_ohlc
from ea_py.types import OhlcBar
def build_system_content_block() -> str:
"""通常モード用のシステムメッセージを返す。"""
return (
"あなたは優秀な投資アドバイザーです。"
"ユーザーの指示を厳密に守り、指定された形式の数値のみを出力してください。"
"思考過程や説明文は一切出力してはいけません。"
).strip()
def build_system_content_block_debug() -> str:
"""デバッグモード用のシステムメッセージを返す。"""
return (
"あなたは優秀な投資アドバイザーです。"
"ユーザーの指示を厳密に守ってください。"
"出力は必ず指定されたブロック構造に従ってください。"
"NUMERIC OUTPUT では指定フォーマットの数値行のみ。"
"REASON OUTPUT では理由を文章で簡潔に。"
).strip()
def build_numeric_summary(
current_price: float,
ohlc_short: Sequence[OhlcBar],
ohlc_long: Sequence[OhlcBar],
) -> str:
"""短期・中期H1チャートからプロンプト用の数値要約文を作る。"""
sum_short = summarize_ohlc(ohlc_short)
sum_long = summarize_ohlc(ohlc_long)
return f"""
【数値要約(チャートと同一データ)】
- 現在価格: {current_price:.2f}
- 短期({sum_short["n"]}本):
高値={sum_short["high"]:.2f}, 安値={sum_short["low"]:.2f}, レンジ={sum_short["range"]:.2f},
EATR={sum_short["eatr"]:.2f}, 基準EATR={sum_short["eatr_baseline"]:.2f}, EATR比={sum_short["eatr_ratio"]:.2f},
直近足レンジ={sum_short["latest_range"]:.2f}, 直近足レンジ/EATR={sum_short["latest_range_to_eatr"]:.2f},
上ヒゲ={sum_short["latest_upper_wick"]:.2f}, 下ヒゲ={sum_short["latest_lower_wick"]:.2f},
平均実体={sum_short["avg_body"]:.2f},
上昇本数={sum_short["up"]}, 下落本数={sum_short["down"]}, 傾き={sum_short["slope"]:.4f}
- 中期({sum_long["n"]}本):
高値={sum_long["high"]:.2f}, 安値={sum_long["low"]:.2f}, レンジ={sum_long["range"]:.2f},
EATR={sum_long["eatr"]:.2f}, 基準EATR={sum_long["eatr_baseline"]:.2f}, EATR比={sum_long["eatr_ratio"]:.2f},
直近足レンジ={sum_long["latest_range"]:.2f}, 直近足レンジ/EATR={sum_long["latest_range_to_eatr"]:.2f},
上ヒゲ={sum_long["latest_upper_wick"]:.2f}, 下ヒゲ={sum_long["latest_lower_wick"]:.2f},
平均実体={sum_long["avg_body"]:.2f},
上昇本数={sum_long["up"]}, 下落本数={sum_long["down"]}, 傾き={sum_long["slope"]:.4f}
""".strip()
def build_header(
current_price: float,
numeric_summary: str,
trend_state: int,
candle_short: int = CANDLE_SHORT,
candle_long: int = CANDLE_LONG,
) -> str:
"""現在価格、画像説明、H4 market_stateを含むヘッダー文を作る。"""
header = f"""
以下はXAUUSDGOLD)の{ENTRY_TIMEFRAME}足チャート画像です。
- 1枚目:短期(直近{candle_short}本)
- 2枚目:中期(直近{candle_long}本)
現在価格は {current_price:.2f}(短期チャートの最後の足の終値)とします。
【重要:外部環境判定(H4)】
market_state = {trend_state}{MARKET_STATE_LABELS.get(trend_state, "UNKNOWN")}
この market_state は外部ロジックで確定した前提情報です。必ず尊重してください。
- 0 = 低ボラレンジ: レンジ端からの逆張りのみ候補
- 1 = 高ボラレンジ: レンジ端かつ反転根拠が強い逆張りのみ候補
- 2 = 低ボラ上昇: 買い優先
- 3 = 高ボラ上昇: 買い優先、売り逆張りは禁止
- 4 = 低ボラ下降: 売り優先
- 5 = 高ボラ下降: 売り優先、買い逆張りは禁止
- 6 = 技術エラー停止: Python/CSV/API/パース失敗時のみ。新規注文停止
【H1の役割】
- H1では、H4 market_state と整合する方向・セットアップだけを候補にしてください。
- H1がH4方向と明確に逆行、またはレンジ中央で優位性が弱い場合は見送ってください。
- 実際の発注タイミングはEA側のM15確定足フィルターで確認します。
H1ではM15の細かな反転を先読みせず、1時間以内に到達しうる妥当な候補価格を重視してください。
""".strip()
if numeric_summary:
header = f"{header}\n\n{numeric_summary}".strip()
return header
def build_imbalance_guidance(analysis: ImbalanceAnalysis) -> str:
"""Pythonで決定済みのH1インバランス判定をGPTへ伝える補助ルールを返す。"""
summary = format_imbalance_summary(analysis)
if analysis.signal == "BUY":
signal_rule = (
"- H1では買い方向の初動が検出されています。"
"ただしH4 market_stateと整合する戦略だけを維持し、H4と逆方向の直接候補は出さないでください。"
)
elif analysis.signal == "SELL":
signal_rule = (
"- H1では売り方向の初動が検出されています。"
"ただしH4 market_stateと整合する戦略だけを維持し、H4と逆方向の直接候補は出さないでください。"
)
else:
signal_rule = "- H1では明確なインバランス初動は検出されていません。既存のmarket_state別ルールを優先してください。"
return f"""
【H1インバランス判定(Pythonの数値ロジックで確定済み)】
- {summary}
{signal_rule}
- インバランス有無をあなたが再判定しないでください。
- 実際の発注直前にはEA側のM15確定足フィルターが別途確認します。
""".strip()
def build_market_state_guidance(trend_state: int) -> str:
"""H4 market_state別のH1候補生成ルールを返す。
H1は候補価格作成だけを担当し、H4環境と矛盾する方向は出さない。
レンジでは端からの逆張り条件を厳しくし、トレンドではH4方向に沿う戦略だけを
許可する。高ボラトレンドでは逆方向の逆張りを禁止し、異常ボラや未知状態では
新規停止を指示する。
"""
if trend_state == 0:
return """
【market_state別の追加条件】
- 低ボラレンジのため、T2/T4はレンジ上限・下限に十分近い場合だけ候補にしてください。
- 価格がレンジ中央付近なら、対象戦略を 0.00,0.00,0.00 で見送ってください。
""".strip()
if trend_state == 1:
return """
【market_state別の追加条件】
- 高ボラレンジのため、逆張り条件を通常より厳しくしてください。
- T2はレンジ下限付近、下ヒゲ、ブレイク直後ではないことを重視してください。
- T4はレンジ上限付近、上ヒゲ、ブレイク直後ではないことを重視してください。
- 直近足レンジ/EATRが大きすぎる、または端を強く抜けた直後なら、対象戦略を 0.00,0.00,0.00 で見送ってください。
""".strip()
if trend_state == 3:
return """
【market_state別の追加条件】
- 高ボラ上昇のため、売り方向の逆張りは禁止です。
- T1は上方向ブレイク、T2は浅い押し目買いだけを候補にしてください。
- 深い押し目や急落直後で買い根拠が弱い場合は、対象戦略を 0.00,0.00,0.00 で見送ってください。
""".strip()
if trend_state == 5:
return """
【market_state別の追加条件】
- 高ボラ下降のため、買い方向の逆張りは禁止です。
- T3は下方向ブレイク、T4は浅い戻り売りだけを候補にしてください。
- 深い戻りや急騰直後で売り根拠が弱い場合は、対象戦略を 0.00,0.00,0.00 で見送ってください。
""".strip()
if trend_state == 2:
return """
【market_state別の追加条件】
- 低ボラ上昇のため、買い方向のみ候補にしてください。
- T1は上方向ブレイク、T2は押し目買いとして妥当な距離だけを候補にしてください。
""".strip()
if trend_state == 4:
return """
【market_state別の追加条件】
- 低ボラ下降のため、売り方向のみ候補にしてください。
- T3は下方向ブレイク、T4は戻り売りとして妥当な距離だけを候補にしてください。
""".strip()
return """
【market_state別の追加条件】
- 技術エラー停止または不明な状態のため、新規注文は停止してください。
""".strip()
def build_strategy_distance_rules(
selected_strategies: Sequence[int],
max_entry_distance: float | dict[int, float] | None,
) -> str:
"""戦略別の現在価格からの距離制限ルール文を返す。"""
if max_entry_distance is None:
return ""
if isinstance(max_entry_distance, dict):
lines = []
for strategy in selected_strategies:
distance = max_entry_distance.get(strategy)
if distance is None or distance <= 0.0:
continue
lines.append(
f"- 戦略{strategy}のエントリー基準価格は現在価格から最大 {distance:.2f} 以内にしてください。"
)
if not lines:
return ""
return "\n".join(lines) + "\n この範囲を超える深い指値・遠いブレイク待ちは、遅延エントリーになりやすいため見送ってください。"
if max_entry_distance <= 0.0:
return ""
return (
f"- エントリー基準価格は現在価格から最大 {max_entry_distance:.2f} 以内にしてください。"
"この範囲を超える深い指値・遠いブレイク待ちは、遅延エントリーになりやすいため見送ってください。"
)
def build_common_rules_block(
selected_strategies: Sequence[int],
max_entry_distance: float | dict[int, float] | None = None,
) -> str:
"""選択戦略に応じた価格決定ルールとGPT出力形式を作る。
`selected_strategies` にはH4 market_stateと整合する戦略番号だけを渡す。
GPTには対象戦略の行だけを、`戦略番号,entry,tp,sl,zone_low,zone_high` の数値行で返すよう指定する。
各戦略のentry/tp/slの大小関係、1時間以内の到達条件、12時間以内の決済目線、
条件が弱い場合に `0.00,0.00,0.00,0.00,0.00` で見送るルールもここで明示する。
出力の実検証は `parse_lines_to_13_allow_subset` と `sanitize_numeric_list` が担当する。
"""
descriptions = {
1: "1. 順張りエントリーの買い",
2: "2. 逆張りエントリーの買い",
3: "3. 順張りエントリーの売り",
4: "4. 逆張りエントリーの売り",
}
conditions = {
1: "(エントリー基準 > 現在価格, 利確目標 > エントリー基準, エントリー基準 > ロスカット基準)",
2: "(現在価格 > エントリー基準, 利確目標 > エントリー基準, エントリー基準 > ロスカット基準)",
3: "(現在価格 > エントリー基準, エントリー基準 > 利確目標, ロスカット基準 > エントリー基準)",
4: "(エントリー基準 > 現在価格, エントリー基準 > 利確目標, ロスカット基準 > エントリー基準)",
}
lines = []
for strategy in selected_strategies:
lines.append(f"{descriptions[strategy]}\n {conditions[strategy]}")
strategy_block = "\n\n".join(lines).strip()
out_order = "".join(str(strategy) for strategy in selected_strategies)
distance_rule = build_strategy_distance_rules(selected_strategies, max_entry_distance)
return f"""
このデータを用いて、以下の戦略パターンについてのみ、
それぞれ独立にエントリー条件を検討してください。
【対象戦略】
{strategy_block}
【価格決定ルール】
- 各戦略ごとに、以下の3つの価格を必ず決定してください。
- エントリー基準価格
- 利確目標価格
- ロスカット基準価格
- 各戦略ごとに、エントリー基準価格の周辺にある「予測ゾーン」も必ず決定してください。
- zone_low は予測ゾーンの低い価格
- zone_high は予測ゾーンの高い価格
- エントリー基準価格は必ず zone_low 以上 zone_high 以下にしてください。
- 各価格の大小関係が、その戦略の条件と整合しているか必ず検証してください。
- それぞれの戦略において、利益の期待値が最大になるように価格を設定してください。
- 条件が弱い、ブレイク直後、レンジ中央付近、EATR基準でリスクが大きすぎる等で見送る場合は、
その戦略行を「戦略番号,0.00,0.00,0.00,0.00,0.00」としてください。
{distance_rule}
【時間条件(全戦略共通)】
- 現在価格から1時間以内にエントリー基準価格に到達しなければ、その戦略はキャンセル。
- 実際の発注はEA側でM15確定足の勢い・反転・候補価格への接近を確認してから行う。
- エントリー後12時間以内に利確・損切に到達しなければ、その時点の価格でクローズ。
【出力ルール(最重要)】
以下の形式で **対象戦略の行だけ** 出力してください。
- 1行につき1戦略
- 行の順序は {out_order}
- 各行は以下の6つをカンマ区切りで出力
戦略番号,エントリー基準価格,利確目標価格,ロスカット基準価格,予測ゾーン下限,予測ゾーン上限
- 数値のみを出力し、説明文・空行・記号は一切出力してはいけません。
- 各行の価格は必ず小数点以下2桁まで出力すること(例: 4812.62)。
- 見送り行も必ず小数点以下2桁の 0.00 を使うこと。
""".strip()
def build_common_rules_block_debug(
selected_strategies: Sequence[int],
max_entry_distance: float | dict[int, float] | None = None,
) -> str:
"""理由出力を含む価格決定ルールと出力形式を作る。"""
base = build_common_rules_block(selected_strategies, max_entry_distance)
tail = """
【デバッグ追加ルール】
デバッグモードのため、出力を次の3ブロック構成にしてください(順番固定)。
### NUMERIC OUTPUT ###
ここには、上記「出力ルール(最重要)」に従った“数値行のみ”をそのまま出力してください。
(余計な文字や空行は禁止)
### REASON OUTPUT ###
各出力行について、entry/tp/sl をそのように置いた意図を各1〜2行で説明してください。
最後に「market_state をどう解釈したか」を1〜2行でまとめてください。
### END ###
""".strip()
return f"{base}\n\n{tail}".strip()
def build_caution_block() -> str:
"""market_stateとH1判断が矛盾した場合の安全側ルールを返す。"""
return """
※ market_state とチャート/数値要約が矛盾すると判断した場合は、安全側に倒してください。
具体的には対象戦略を 0.00,0.00,0.00 で見送ってください。
""".strip()
@@ -0,0 +1,100 @@
"""H4トレンド判定用のプロンプトを生成する。"""
from __future__ import annotations
from typing import Sequence
from ea_py.market.volatility import summarize_ohlc
from ea_py.types import OhlcBar
def build_trend_system_content() -> str:
"""通常モード用のシステムメッセージを返す。"""
return (
"あなたは相場分析の判定器です。"
"出力は数字1つ(0/1/2)のみ。"
"説明文、記号、空行、追加の数値は一切出力しない。"
"出力は必ず 0 または 1 または 2。"
).strip()
def build_trend_system_content_debug() -> str:
"""デバッグモード用のシステムメッセージを返す。"""
return (
"あなたは相場分析の判定器です。"
"ユーザーの指示を厳密に守ってください。"
"出力は必ず指定されたブロック構造に従ってください。"
"NUMERIC OUTPUT では 0/1/2 の数字1つのみ。"
"REASON OUTPUT では理由を文章で簡潔に。"
).strip()
def build_trend_numeric_summary(current_price: float, ohlc_trend: Sequence[OhlcBar]) -> str:
"""H4チャートと同一データ由来の数値要約文を作る。"""
summary = summarize_ohlc(ohlc_trend)
return f"""
【数値要約(チャートと同一データ)】
- 現在価格: {current_price:.2f}
- 対象本数: {summary["n"]}本(H4
高値={summary["high"]:.2f}, 安値={summary["low"]:.2f}, レンジ={summary["range"]:.2f},
EATR={summary["eatr"]:.2f}, 基準EATR={summary["eatr_baseline"]:.2f}, EATR比={summary["eatr_ratio"]:.2f},
直近足レンジ={summary["latest_range"]:.2f}, 直近足レンジ/EATR={summary["latest_range_to_eatr"]:.2f},
上ヒゲ={summary["latest_upper_wick"]:.2f}, 下ヒゲ={summary["latest_lower_wick"]:.2f},
平均実体={summary["avg_body"]:.2f},
上昇本数={summary["up"]}, 下落本数={summary["down"]}, 傾き={summary["slope"]:.4f}
""".strip()
def build_trend_user_prompt(current_price: float, numeric_summary: str, candle_count: int) -> str:
"""H4方向判定用のユーザープロンプトを作る。
GPTにはXAUUSD/GOLDの今後12時間目線で、0=レンジ、1=上昇、2=下降の
いずれか1つだけを返すよう強制する。
チャート画像と同一データ由来の数値要約を併用させるが、方向が混在する場合は
事故回避としてレンジ(0)を選ぶよう指示する。
この関数はプロンプト文字列を作るだけで、OpenAI呼び出しや出力検証は行わない。
"""
return f"""
あなたはXAUUSD(GOLD)のH4の今後12時間のトレンドを判定し、
次のいずれかを **数字1つ** で出力してください。
0 = 横ばい(レンジ)
1 = 上昇トレンド
2 = 下降トレンド
【入力】
- 現在価格: {current_price:.2f}
- 添付のH4チャート画像(直近{candle_count}本)
- 数値要約(同一データ由来):
{numeric_summary}
【判定の考え方】
- 画像と数値要約の両方を参考にして良い。
- 判定ロジック(傾き、ATR、MA、ダウ理論、レンジ幅など)はあなたが最適だと思う方法でよい。
- ただし「自信がない」「方向が混在している」場合は事故回避のため 0(横ばい)を選ぶこと。
【出力ルール】
- 出力は 0 / 1 / 2 のどれか数字1つのみ。
- それ以外は一切出力しない。
""".strip()
def build_trend_user_prompt_debug(current_price: float, numeric_summary: str, candle_count: int) -> str:
"""理由出力を含むH4トレンド判定ユーザープロンプトを作る。"""
base = build_trend_user_prompt(current_price, numeric_summary, candle_count)
tail = """
【デバッグ追加ルール】
デバッグモードのため、出力を次の3ブロック構成にしてください(順番固定)。
### NUMERIC OUTPUT ###
0 / 1 / 2 のどれか数字1つのみ
(余計な文字、空行、記号は禁止)
### REASON OUTPUT ###
なぜその判定(0/1/2)にしたかを、箇条書きで3〜6点で簡潔に。
最後に「12時間目線での注意点」を1行で。
### END ###
""".strip()
return f"{base}\n\n{tail}".strip()
+38
View File
@@ -0,0 +1,38 @@
"""プロジェクト内で共有する型定義。"""
from __future__ import annotations
from typing import TypedDict
class OhlcBar(TypedDict):
"""MT5 OHLC CSVから読み込んだ1本分のローソク足。"""
DateTime: str
Open: float
High: float
Low: float
Close: float
class OhlcSummary(TypedDict):
"""ローソク足配列から算出した数値要約。"""
n: int
high: float
low: float
range: float
eatr: float
eatr_baseline: float
eatr_ratio: float
latest_range: float
latest_range_to_eatr: float
latest_upper_wick: float
latest_lower_wick: float
avg_body: float
up: int
down: int
slope: float
NumericList = list[int | float]