Consolidate Python ignore rules into root gitignore
This commit is contained in:
@@ -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}"
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
"""I/O helpers for MT5 integration files."""
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"""
|
||||
以下はXAUUSD(GOLD)の{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()
|
||||
@@ -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]
|
||||
Reference in New Issue
Block a user