Adopt structured outputs for GPT entry generation
This commit is contained in:
@@ -92,7 +92,8 @@ MT5 が読む結果ファイルは `utf-16 LE` で出力します。結果ファ
|
|||||||
3. H1 の短期 36 本、中期 72 本からチャート PNG と数値要約を作る。
|
3. H1 の短期 36 本、中期 72 本からチャート PNG と数値要約を作る。
|
||||||
4. H1 インバランス初動を Python 側で判定し、矛盾する候補を抑止する。
|
4. H1 インバランス初動を Python 側で判定し、矛盾する候補を抑止する。
|
||||||
5. OpenAI Responses API に候補価格・予測ゾーン生成を依頼する。
|
5. OpenAI Responses API に候補価格・予測ゾーン生成を依頼する。
|
||||||
6. GPT 出力を既存13行形式と分割エントリー用ゾーン形式へ展開し、価格の大小関係と距離を検証する。
|
出力は Structured Outputs のJSON Schemaで固定する。
|
||||||
|
6. GPT JSON出力を既存13行形式と分割エントリー用ゾーン形式へ展開し、価格の大小関係、距離、reward/riskを検証する。
|
||||||
7. `target_prices.txt` と `target_zones.txt` を書き、両方の完了後に `process_done_entry.txt` を作成する。
|
7. `target_prices.txt` と `target_zones.txt` を書き、両方の完了後に `process_done_entry.txt` を作成する。
|
||||||
|
|
||||||
H4 状態ごとの許可戦略:
|
H4 状態ごとの許可戦略:
|
||||||
@@ -132,6 +133,9 @@ EA側で分割エントリーを有効にした場合は、この予測ゾーン
|
|||||||
## OpenAI 設定
|
## OpenAI 設定
|
||||||
|
|
||||||
H1 エントリー候補生成では OpenAI API を使用します。
|
H1 エントリー候補生成では OpenAI API を使用します。
|
||||||
|
Responses APIの `text.format` にJSON Schemaを渡し、GPT出力を自然文やCSV風テキストではなく
|
||||||
|
`schema_version` と `strategies` を持つ構造化JSONに固定します。
|
||||||
|
JSONが不正、API応答が未完了、価格条件やreward/risk条件に合わない場合は停止値へ倒します。
|
||||||
|
|
||||||
必須:
|
必須:
|
||||||
|
|
||||||
|
|||||||
@@ -207,8 +207,8 @@ EAファイルはこのPythonプロジェクトの外部連携先として扱う
|
|||||||
| `src/ea_py/charting/candlestick.py` | ローソク足PNG生成。 |
|
| `src/ea_py/charting/candlestick.py` | ローソク足PNG生成。 |
|
||||||
| `src/ea_py/market/volatility.py` | True Range、Exponential ATR、ボラティリティ分類。 |
|
| `src/ea_py/market/volatility.py` | True Range、Exponential ATR、ボラティリティ分類。 |
|
||||||
| `src/ea_py/market/trend_state.py` | H4方向判定結果とボラティリティ分類の合成。 |
|
| `src/ea_py/market/trend_state.py` | H4方向判定結果とボラティリティ分類の合成。 |
|
||||||
| `src/ea_py/market/target_prices.py` | GPT出力のパース、価格整合性チェック、13行形式への変換。 |
|
| `src/ea_py/market/target_prices.py` | 既存13行形式への変換と価格整合性チェック。 |
|
||||||
| `src/ea_py/market/target_zones.py` | GPT出力の予測ゾーンを分割エントリー用7行形式へ変換。 |
|
| `src/ea_py/market/target_zones.py` | Structured OutputsのGPT JSON出力を予測ゾーンへ変換し、分割エントリー用7行形式へ展開。 |
|
||||||
| `src/ea_py/market/imbalance.py` | H1インバランス初動判定。 |
|
| `src/ea_py/market/imbalance.py` | H1インバランス初動判定。 |
|
||||||
| `src/ea_py/prompts/trend_prompt.py` | H4相場環境判定プロンプトの生成。 |
|
| `src/ea_py/prompts/trend_prompt.py` | H4相場環境判定プロンプトの生成。 |
|
||||||
| `src/ea_py/prompts/entry_prompt.py` | H1候補価格生成プロンプトの生成。 |
|
| `src/ea_py/prompts/entry_prompt.py` | H1候補価格生成プロンプトの生成。 |
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ DEFAULT_GPT_MODEL = "gpt-5.5-2026-04-23"
|
|||||||
DEFAULT_REASONING_EFFORT = "low"
|
DEFAULT_REASONING_EFFORT = "low"
|
||||||
VALID_REASONING_EFFORTS = frozenset({"none", "low", "medium", "high", "xhigh"})
|
VALID_REASONING_EFFORTS = frozenset({"none", "low", "medium", "high", "xhigh"})
|
||||||
DEBUG_PRINT = True
|
DEBUG_PRINT = True
|
||||||
|
ENTRY_RESPONSE_SCHEMA_VERSION = 1
|
||||||
|
|
||||||
ATR_PERIOD = 14
|
ATR_PERIOD = 14
|
||||||
ATR_BASELINE_PERIOD = 20
|
ATR_BASELINE_PERIOD = 20
|
||||||
@@ -24,6 +25,7 @@ IMBALANCE_MIN_AVG_BODY_SIZE = 0.01
|
|||||||
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER = 1.00
|
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER = 1.00
|
||||||
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER = 1.50
|
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER = 1.50
|
||||||
ENTRY_MAX_DISTANCE_MIN_PRICE = 5.0
|
ENTRY_MAX_DISTANCE_MIN_PRICE = 5.0
|
||||||
|
MIN_ENTRY_REWARD_RISK_RATIO = 1.20
|
||||||
|
|
||||||
CANDLE_TREND = 72
|
CANDLE_TREND = 72
|
||||||
CANDLE_SHORT = 36
|
CANDLE_SHORT = 36
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -30,6 +30,10 @@ class ResponsesApiDiagnostics:
|
|||||||
error: str
|
error: str
|
||||||
usage: str
|
usage: str
|
||||||
|
|
||||||
|
def is_completed(self) -> bool:
|
||||||
|
"""ライブ判定に使える完了レスポンスかどうかを返す。"""
|
||||||
|
return self.status == "completed" and not self.incomplete_details and not self.error
|
||||||
|
|
||||||
def to_log_text(self) -> str:
|
def to_log_text(self) -> str:
|
||||||
"""デバッグログへそのまま書ける1行診断文字列を返す。"""
|
"""デバッグログへそのまま書ける1行診断文字列を返す。"""
|
||||||
parts = [
|
parts = [
|
||||||
@@ -89,6 +93,7 @@ def call_responses_api(
|
|||||||
user_text: str,
|
user_text: str,
|
||||||
image_data_urls: Sequence[str],
|
image_data_urls: Sequence[str],
|
||||||
max_output_tokens: int,
|
max_output_tokens: int,
|
||||||
|
response_text_format: Mapping[str, Any] | None = None,
|
||||||
) -> ResponsesApiResult:
|
) -> ResponsesApiResult:
|
||||||
"""Responses APIへテキストとチャート画像を送り、出力テキストを返す。
|
"""Responses APIへテキストとチャート画像を送り、出力テキストを返す。
|
||||||
|
|
||||||
@@ -96,6 +101,9 @@ def call_responses_api(
|
|||||||
`user_text` はH4/H1の具体的な分析依頼、数値要約、出力フォーマットを含む。
|
`user_text` はH4/H1の具体的な分析依頼、数値要約、出力フォーマットを含む。
|
||||||
`image_data_urls` にはPNGをdata URL化したチャート画像を渡す。空文字は無視する。
|
`image_data_urls` にはPNGをdata URL化したチャート画像を渡す。空文字は無視する。
|
||||||
|
|
||||||
|
`response_text_format` が指定された場合は Responses API の `text.format` へ渡し、
|
||||||
|
JSON Schemaなどの構造化出力をAPI側でも強制する。
|
||||||
|
|
||||||
戻り値は `response.output_text` をstripした文字列とAPI診断情報。
|
戻り値は `response.output_text` をstripした文字列とAPI診断情報。
|
||||||
API例外や空/不正な出力の安全側処理は、この薄いラッパーではなく
|
API例外や空/不正な出力の安全側処理は、この薄いラッパーではなく
|
||||||
呼び出し元のパイプラインとパース関数が担当する。
|
呼び出し元のパイプラインとパース関数が担当する。
|
||||||
@@ -121,6 +129,9 @@ def call_responses_api(
|
|||||||
"reasoning": {"effort": reasoning_effort},
|
"reasoning": {"effort": reasoning_effort},
|
||||||
"max_output_tokens": max_output_tokens,
|
"max_output_tokens": max_output_tokens,
|
||||||
}
|
}
|
||||||
|
if response_text_format is not None:
|
||||||
|
create_params["text"] = {"format": dict(response_text_format)}
|
||||||
|
|
||||||
response = client.responses.create(**create_params)
|
response = client.responses.create(**create_params)
|
||||||
diagnostics = _extract_response_diagnostics(response)
|
diagnostics = _extract_response_diagnostics(response)
|
||||||
text = (response.output_text or "").strip()
|
text = (response.output_text or "").strip()
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from ea_py.constants import (
|
|||||||
CANDLE_LONG,
|
CANDLE_LONG,
|
||||||
CANDLE_SHORT,
|
CANDLE_SHORT,
|
||||||
DEBUG_PRINT,
|
DEBUG_PRINT,
|
||||||
ENTRY_DEBUG_MAX_OUTPUT_TOKENS,
|
|
||||||
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER,
|
ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER,
|
||||||
ENTRY_MAX_DISTANCE_MIN_PRICE,
|
ENTRY_MAX_DISTANCE_MIN_PRICE,
|
||||||
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER,
|
ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER,
|
||||||
@@ -22,6 +21,7 @@ from ea_py.constants import (
|
|||||||
IMBALANCE_SENSITIVITY,
|
IMBALANCE_SENSITIVITY,
|
||||||
INSTRUMENT,
|
INSTRUMENT,
|
||||||
MARKET_STATE_LABELS,
|
MARKET_STATE_LABELS,
|
||||||
|
MIN_ENTRY_REWARD_RISK_RATIO,
|
||||||
MT_ENCODING,
|
MT_ENCODING,
|
||||||
TECHNICAL_ERROR_STOP,
|
TECHNICAL_ERROR_STOP,
|
||||||
USE_IMBALANCE_FILTER,
|
USE_IMBALANCE_FILTER,
|
||||||
@@ -31,14 +31,13 @@ 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.io.ohlc_csv import read_ohlc_csv
|
||||||
from ea_py.market.volatility import summarize_ohlc
|
from ea_py.market.volatility import summarize_ohlc
|
||||||
from ea_py.market.target_prices import (
|
from ea_py.market.target_prices import (
|
||||||
extract_entry_blocks_debug,
|
|
||||||
stop_numeric_list,
|
stop_numeric_list,
|
||||||
strategies_by_trend,
|
strategies_by_trend,
|
||||||
)
|
)
|
||||||
from ea_py.market.target_zones import (
|
from ea_py.market.target_zones import (
|
||||||
build_candidate_id,
|
build_candidate_id,
|
||||||
format_target_zones,
|
format_target_zones,
|
||||||
parse_lines_to_entry_zones_allow_subset,
|
parse_json_to_entry_zones,
|
||||||
sanitize_entry_zones,
|
sanitize_entry_zones,
|
||||||
stop_entry_zones,
|
stop_entry_zones,
|
||||||
zones_to_numeric_list,
|
zones_to_numeric_list,
|
||||||
@@ -54,7 +53,7 @@ from ea_py.prompts.entry_prompt import (
|
|||||||
build_imbalance_guidance,
|
build_imbalance_guidance,
|
||||||
build_caution_block,
|
build_caution_block,
|
||||||
build_common_rules_block,
|
build_common_rules_block,
|
||||||
build_common_rules_block_debug,
|
build_entry_response_text_format,
|
||||||
build_header,
|
build_header,
|
||||||
build_market_state_guidance,
|
build_market_state_guidance,
|
||||||
build_numeric_summary,
|
build_numeric_summary,
|
||||||
@@ -206,7 +205,8 @@ def run_pipeline() -> None:
|
|||||||
f"h1_eatr={h1_eatr:.2f}, "
|
f"h1_eatr={h1_eatr:.2f}, "
|
||||||
f"stop_multiplier={ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER:.2f}, "
|
f"stop_multiplier={ENTRY_MAX_DISTANCE_STOP_EATR_MULTIPLIER:.2f}, "
|
||||||
f"limit_multiplier={ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER:.2f}, "
|
f"limit_multiplier={ENTRY_MAX_DISTANCE_LIMIT_EATR_MULTIPLIER:.2f}, "
|
||||||
f"floor={ENTRY_MAX_DISTANCE_MIN_PRICE:.2f}"
|
f"floor={ENTRY_MAX_DISTANCE_MIN_PRICE:.2f}, "
|
||||||
|
f"min_reward_risk={MIN_ENTRY_REWARD_RISK_RATIO:.2f}"
|
||||||
)
|
)
|
||||||
header = build_header(
|
header = build_header(
|
||||||
current_price=current_price,
|
current_price=current_price,
|
||||||
@@ -224,14 +224,10 @@ def run_pipeline() -> None:
|
|||||||
write_entry_output(stop_numeric_list())
|
write_entry_output(stop_numeric_list())
|
||||||
return
|
return
|
||||||
|
|
||||||
if config.debug_print:
|
system_content = build_system_content_block_debug() if config.debug_print else build_system_content_block()
|
||||||
system_content = build_system_content_block_debug()
|
common_rules = build_common_rules_block(selected_strategies, max_entry_distance)
|
||||||
common_rules = build_common_rules_block_debug(selected_strategies, max_entry_distance)
|
response_text_format = build_entry_response_text_format(selected_strategies)
|
||||||
max_tokens = ENTRY_DEBUG_MAX_OUTPUT_TOKENS
|
max_tokens = ENTRY_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()
|
user_text = "\n\n".join([header, market_state_guidance, imbalance_guidance, common_rules, caution]).strip()
|
||||||
|
|
||||||
@@ -246,9 +242,14 @@ def run_pipeline() -> None:
|
|||||||
user_text=user_text,
|
user_text=user_text,
|
||||||
image_data_urls=images_data_urls,
|
image_data_urls=images_data_urls,
|
||||||
max_output_tokens=max_tokens,
|
max_output_tokens=max_tokens,
|
||||||
|
response_text_format=response_text_format,
|
||||||
)
|
)
|
||||||
gpt_reply = gpt_result.text
|
gpt_reply = gpt_result.text
|
||||||
api_diagnostics = gpt_result.diagnostics.to_log_text()
|
api_diagnostics = gpt_result.diagnostics.to_log_text()
|
||||||
|
if not gpt_result.diagnostics.is_completed():
|
||||||
|
logger.error("OpenAI response incomplete or failed: %s", api_diagnostics)
|
||||||
|
write_entry_output(stop_numeric_list())
|
||||||
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("OpenAI APIエラー")
|
logger.exception("OpenAI APIエラー")
|
||||||
write_entry_output(stop_numeric_list())
|
write_entry_output(stop_numeric_list())
|
||||||
@@ -262,21 +263,18 @@ def run_pipeline() -> None:
|
|||||||
gpt_reply,
|
gpt_reply,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
entry_zones = parse_json_to_entry_zones(gpt_reply)
|
||||||
|
entry_zones = sanitize_entry_zones(
|
||||||
|
entry_zones,
|
||||||
|
selected_strategies,
|
||||||
|
current_price,
|
||||||
|
max_entry_distance=max_entry_distance,
|
||||||
|
min_reward_risk_ratio=MIN_ENTRY_REWARD_RISK_RATIO,
|
||||||
|
)
|
||||||
|
numeric_list = zones_to_numeric_list(entry_zones)
|
||||||
|
zone_text = format_target_zones(entry_zones, candidate_id)
|
||||||
|
|
||||||
if config.debug_print:
|
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:
|
try:
|
||||||
append_debug_entry(
|
append_debug_entry(
|
||||||
path=paths.debug_reason,
|
path=paths.debug_reason,
|
||||||
@@ -290,23 +288,13 @@ def run_pipeline() -> None:
|
|||||||
selected_strategies=selected_strategies,
|
selected_strategies=selected_strategies,
|
||||||
imbalance_summary=imbalance_summary,
|
imbalance_summary=imbalance_summary,
|
||||||
numeric_summary=numeric_summary,
|
numeric_summary=numeric_summary,
|
||||||
numeric_lines=numeric_lines,
|
numeric_lines=gpt_reply,
|
||||||
post_filter_summary=post_filter_summary,
|
post_filter_summary=post_filter_summary,
|
||||||
sanitized_numeric_list=numeric_list,
|
sanitized_numeric_list=numeric_list,
|
||||||
reason_text=f"{reason_text}\n\nTARGET_ZONES\n{zone_text}".strip(),
|
reason_text=f"STRUCTURED_JSON\n{gpt_reply}\n\nTARGET_ZONES\n{zone_text}".strip(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("debug_entry.txt write error")
|
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)
|
write_entry_output(numeric_list, zone_text)
|
||||||
|
|
||||||
|
|||||||
@@ -3,33 +3,70 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from ea_py.constants import CANDLE_LONG, CANDLE_SHORT, ENTRY_TIMEFRAME, MARKET_STATE_LABELS
|
from ea_py.constants import CANDLE_LONG, CANDLE_SHORT, ENTRY_RESPONSE_SCHEMA_VERSION, ENTRY_TIMEFRAME, MARKET_STATE_LABELS
|
||||||
from ea_py.market.imbalance import ImbalanceAnalysis, format_imbalance_summary
|
from ea_py.market.imbalance import ImbalanceAnalysis, format_imbalance_summary
|
||||||
from ea_py.market.volatility import summarize_ohlc
|
from ea_py.market.volatility import summarize_ohlc
|
||||||
from ea_py.types import OhlcBar
|
from ea_py.types import OhlcBar, OhlcSummary
|
||||||
|
|
||||||
|
ENTRY_REASON_CODES = (
|
||||||
|
"trend_breakout",
|
||||||
|
"pullback",
|
||||||
|
"range_edge",
|
||||||
|
"range_reversal",
|
||||||
|
"skip_weak_signal",
|
||||||
|
"skip_mid_range",
|
||||||
|
"skip_distance",
|
||||||
|
"skip_risk",
|
||||||
|
"skip_conflict",
|
||||||
|
"skip_other",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_system_content_block() -> str:
|
def build_system_content_block() -> str:
|
||||||
"""通常モード用のシステムメッセージを返す。"""
|
"""通常モード用のシステムメッセージを返す。"""
|
||||||
return (
|
return (
|
||||||
"あなたは優秀な投資アドバイザーです。"
|
"あなたはアルゴリズム取引用の価格候補生成器です。"
|
||||||
"ユーザーの指示を厳密に守り、指定された形式の数値のみを出力してください。"
|
"投資助言や自然文説明ではなく、指定JSON Schemaに一致する機械可読な候補だけを返してください。"
|
||||||
"思考過程や説明文は一切出力してはいけません。"
|
"ユーザーの制約、market_state、選択戦略、価格関係を厳密に守ってください。"
|
||||||
|
"迷う場合は必ずskipにしてください。"
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
def build_system_content_block_debug() -> str:
|
def build_system_content_block_debug() -> str:
|
||||||
"""デバッグモード用のシステムメッセージを返す。"""
|
"""デバッグモード用のシステムメッセージを返す。"""
|
||||||
return (
|
return (
|
||||||
"あなたは優秀な投資アドバイザーです。"
|
"あなたはアルゴリズム取引用の価格候補生成器です。"
|
||||||
"ユーザーの指示を厳密に守ってください。"
|
"指定JSON Schemaに一致する機械可読な候補だけを返してください。"
|
||||||
"出力は必ず指定されたブロック構造に従ってください。"
|
"自然文の理由や思考過程は出力せず、理由はreason_codeだけで表してください。"
|
||||||
"NUMERIC OUTPUT では指定フォーマットの数値行のみ。"
|
|
||||||
"REASON OUTPUT では理由を文章で簡潔に。"
|
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_ratio(numerator: float, denominator: float) -> float:
|
||||||
|
"""0除算を避けて比率を返す。"""
|
||||||
|
if denominator <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return numerator / denominator
|
||||||
|
|
||||||
|
|
||||||
|
def _format_position_metrics(summary: OhlcSummary, current_price: float) -> str:
|
||||||
|
"""レンジ内位置と高安までの距離を要約行へ追加する。"""
|
||||||
|
high = float(summary["high"])
|
||||||
|
low = float(summary["low"])
|
||||||
|
price_range = float(summary["range"])
|
||||||
|
eatr = float(summary["eatr"])
|
||||||
|
range_position = _safe_ratio(current_price - low, price_range)
|
||||||
|
distance_to_high = high - current_price
|
||||||
|
distance_to_low = current_price - low
|
||||||
|
|
||||||
|
return (
|
||||||
|
f" 現在価格レンジ位置={range_position:.2f}(0=安値付近, 1=高値付近), "
|
||||||
|
f"高値まで={distance_to_high:.2f}({_safe_ratio(distance_to_high, eatr):.2f}EATR), "
|
||||||
|
f"安値まで={distance_to_low:.2f}({_safe_ratio(distance_to_low, eatr):.2f}EATR)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_numeric_summary(
|
def build_numeric_summary(
|
||||||
current_price: float,
|
current_price: float,
|
||||||
ohlc_short: Sequence[OhlcBar],
|
ohlc_short: Sequence[OhlcBar],
|
||||||
@@ -50,6 +87,7 @@ def build_numeric_summary(
|
|||||||
上ヒゲ={sum_short["latest_upper_wick"]:.2f}, 下ヒゲ={sum_short["latest_lower_wick"]:.2f},
|
上ヒゲ={sum_short["latest_upper_wick"]:.2f}, 下ヒゲ={sum_short["latest_lower_wick"]:.2f},
|
||||||
平均実体={sum_short["avg_body"]:.2f},
|
平均実体={sum_short["avg_body"]:.2f},
|
||||||
上昇本数={sum_short["up"]}, 下落本数={sum_short["down"]}, 傾き={sum_short["slope"]:.4f}
|
上昇本数={sum_short["up"]}, 下落本数={sum_short["down"]}, 傾き={sum_short["slope"]:.4f}
|
||||||
|
{_format_position_metrics(sum_short, current_price)}
|
||||||
|
|
||||||
- 中期({sum_long["n"]}本):
|
- 中期({sum_long["n"]}本):
|
||||||
高値={sum_long["high"]:.2f}, 安値={sum_long["low"]:.2f}, レンジ={sum_long["range"]:.2f},
|
高値={sum_long["high"]:.2f}, 安値={sum_long["low"]:.2f}, レンジ={sum_long["range"]:.2f},
|
||||||
@@ -58,6 +96,7 @@ def build_numeric_summary(
|
|||||||
上ヒゲ={sum_long["latest_upper_wick"]:.2f}, 下ヒゲ={sum_long["latest_lower_wick"]:.2f},
|
上ヒゲ={sum_long["latest_upper_wick"]:.2f}, 下ヒゲ={sum_long["latest_lower_wick"]:.2f},
|
||||||
平均実体={sum_long["avg_body"]:.2f},
|
平均実体={sum_long["avg_body"]:.2f},
|
||||||
上昇本数={sum_long["up"]}, 下落本数={sum_long["down"]}, 傾き={sum_long["slope"]:.4f}
|
上昇本数={sum_long["up"]}, 下落本数={sum_long["down"]}, 傾き={sum_long["slope"]:.4f}
|
||||||
|
{_format_position_metrics(sum_long, current_price)}
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -91,6 +130,8 @@ market_state = {trend_state}({MARKET_STATE_LABELS.get(trend_state, "UNKNOWN")}
|
|||||||
【H1の役割】
|
【H1の役割】
|
||||||
- H1では、H4 market_state と整合する方向・セットアップだけを候補にしてください。
|
- H1では、H4 market_state と整合する方向・セットアップだけを候補にしてください。
|
||||||
- H1がH4方向と明確に逆行、またはレンジ中央で優位性が弱い場合は見送ってください。
|
- H1がH4方向と明確に逆行、またはレンジ中央で優位性が弱い場合は見送ってください。
|
||||||
|
- 画像と数値要約が矛盾する場合は、バックテスト再現性を優先して数値要約を主根拠にしてください。
|
||||||
|
- 外部ニュース、経済指標、未知のファンダメンタル材料は考慮しないでください。
|
||||||
- 実際の発注タイミングはEA側のM15確定足フィルターで確認します。
|
- 実際の発注タイミングはEA側のM15確定足フィルターで確認します。
|
||||||
H1ではM15の細かな反転を先読みせず、1時間以内に到達しうる妥当な候補価格を重視してください。
|
H1ではM15の細かな反転を先読みせず、1時間以内に到達しうる妥当な候補価格を重視してください。
|
||||||
""".strip()
|
""".strip()
|
||||||
@@ -223,7 +264,7 @@ def build_common_rules_block(
|
|||||||
"""選択戦略に応じた価格決定ルールとGPT出力形式を作る。
|
"""選択戦略に応じた価格決定ルールとGPT出力形式を作る。
|
||||||
|
|
||||||
`selected_strategies` にはH4 market_stateと整合する戦略番号だけを渡す。
|
`selected_strategies` にはH4 market_stateと整合する戦略番号だけを渡す。
|
||||||
GPTには対象戦略の行だけを、`戦略番号,entry,tp,sl,zone_low,zone_high` の数値行で返すよう指定する。
|
GPTには対象戦略の候補だけを、JSON Schemaに一致するオブジェクトで返すよう指定する。
|
||||||
各戦略のentry/tp/slの大小関係、1時間以内の到達条件、12時間以内の決済目線、
|
各戦略のentry/tp/slの大小関係、1時間以内の到達条件、12時間以内の決済目線、
|
||||||
条件が弱い場合に `0.00,0.00,0.00,0.00,0.00` で見送るルールもここで明示する。
|
条件が弱い場合に `0.00,0.00,0.00,0.00,0.00` で見送るルールもここで明示する。
|
||||||
|
|
||||||
@@ -267,9 +308,10 @@ def build_common_rules_block(
|
|||||||
- zone_high は予測ゾーンの高い価格
|
- zone_high は予測ゾーンの高い価格
|
||||||
- エントリー基準価格は必ず zone_low 以上 zone_high 以下にしてください。
|
- エントリー基準価格は必ず zone_low 以上 zone_high 以下にしてください。
|
||||||
- 各価格の大小関係が、その戦略の条件と整合しているか必ず検証してください。
|
- 各価格の大小関係が、その戦略の条件と整合しているか必ず検証してください。
|
||||||
- それぞれの戦略において、利益の期待値が最大になるように価格を設定してください。
|
- reward/risk が 1.20 未満になる候補は、期待値が弱いため見送ってください。
|
||||||
|
- SL距離がH1の値幅に対して広すぎる、またはTPまでの余地が小さすぎる候補は見送ってください。
|
||||||
- 条件が弱い、ブレイク直後、レンジ中央付近、EATR基準でリスクが大きすぎる等で見送る場合は、
|
- 条件が弱い、ブレイク直後、レンジ中央付近、EATR基準でリスクが大きすぎる等で見送る場合は、
|
||||||
その戦略行を「戦略番号,0.00,0.00,0.00,0.00,0.00」としてください。
|
decision を "skip" とし、entry/tp/sl/zone_low/zone_high をすべて 0.0 にしてください。
|
||||||
{distance_rule}
|
{distance_rule}
|
||||||
|
|
||||||
【時間条件(全戦略共通)】
|
【時間条件(全戦略共通)】
|
||||||
@@ -278,17 +320,13 @@ def build_common_rules_block(
|
|||||||
- エントリー後12時間以内に利確・損切に到達しなければ、その時点の価格でクローズ。
|
- エントリー後12時間以内に利確・損切に到達しなければ、その時点の価格でクローズ。
|
||||||
|
|
||||||
【出力ルール(最重要)】
|
【出力ルール(最重要)】
|
||||||
以下の形式で **対象戦略の行だけ** 出力してください。
|
- Responses APIのJSON Schemaに一致するJSONオブジェクトだけを出力してください。
|
||||||
|
- schema_version は {ENTRY_RESPONSE_SCHEMA_VERSION}。
|
||||||
- 1行につき1戦略
|
- strategies には対象戦略だけを入れ、順序は {out_order}。
|
||||||
- 行の順序は {out_order}
|
- 各strategy要素は strategy, decision, entry, tp, sl, zone_low, zone_high, reason_code を必ず含めてください。
|
||||||
- 各行は以下の6つをカンマ区切りで出力
|
- decision は "use" または "skip" のみ。
|
||||||
|
- reason_code は次のいずれかのみ: {", ".join(ENTRY_REASON_CODES)}
|
||||||
戦略番号,エントリー基準価格,利確目標価格,ロスカット基準価格,予測ゾーン下限,予測ゾーン上限
|
- 自然文、Markdown、余分なキー、空行は一切出力しないでください。
|
||||||
|
|
||||||
- 数値のみを出力し、説明文・空行・記号は一切出力してはいけません。
|
|
||||||
- 各行の価格は必ず小数点以下2桁まで出力すること(例: 4812.62)。
|
|
||||||
- 見送り行も必ず小数点以下2桁の 0.00 を使うこと。
|
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
@@ -296,23 +334,8 @@ def build_common_rules_block_debug(
|
|||||||
selected_strategies: Sequence[int],
|
selected_strategies: Sequence[int],
|
||||||
max_entry_distance: float | dict[int, float] | None = None,
|
max_entry_distance: float | dict[int, float] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""理由出力を含む価格決定ルールと出力形式を作る。"""
|
"""デバッグ時もライブと同じStructured Outputsルールを使う。"""
|
||||||
base = build_common_rules_block(selected_strategies, max_entry_distance)
|
return 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:
|
def build_caution_block() -> str:
|
||||||
@@ -321,3 +344,50 @@ def build_caution_block() -> str:
|
|||||||
※ market_state とチャート/数値要約が矛盾すると判断した場合は、安全側に倒してください。
|
※ market_state とチャート/数値要約が矛盾すると判断した場合は、安全側に倒してください。
|
||||||
具体的には対象戦略を 0.00,0.00,0.00 で見送ってください。
|
具体的には対象戦略を 0.00,0.00,0.00 で見送ってください。
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
def build_entry_response_text_format(selected_strategies: Sequence[int]) -> dict[str, Any]:
|
||||||
|
"""Responses API Structured Outputs用のJSON Schemaを返す。"""
|
||||||
|
strategy_enum = [int(strategy) for strategy in selected_strategies if strategy in (1, 2, 3, 4)]
|
||||||
|
if not strategy_enum:
|
||||||
|
strategy_enum = [1, 2, 3, 4]
|
||||||
|
|
||||||
|
strategy_item_schema: dict[str, Any] = {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"strategy": {"type": "integer", "enum": strategy_enum},
|
||||||
|
"decision": {"type": "string", "enum": ["use", "skip"]},
|
||||||
|
"entry": {"type": "number"},
|
||||||
|
"tp": {"type": "number"},
|
||||||
|
"sl": {"type": "number"},
|
||||||
|
"zone_low": {"type": "number"},
|
||||||
|
"zone_high": {"type": "number"},
|
||||||
|
"reason_code": {"type": "string", "enum": list(ENTRY_REASON_CODES)},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"strategy",
|
||||||
|
"decision",
|
||||||
|
"entry",
|
||||||
|
"tp",
|
||||||
|
"sl",
|
||||||
|
"zone_low",
|
||||||
|
"zone_high",
|
||||||
|
"reason_code",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "json_schema",
|
||||||
|
"name": "hit_ea_entry_candidates",
|
||||||
|
"strict": True,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {"type": "integer", "enum": [ENTRY_RESPONSE_SCHEMA_VERSION]},
|
||||||
|
"strategies": {"type": "array", "items": strategy_item_schema},
|
||||||
|
},
|
||||||
|
"required": ["schema_version", "strategies"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -70,6 +70,36 @@ def test_call_responses_api_sends_reasoning_and_omits_temperature() -> None:
|
|||||||
assert "temperature" not in fake_client.responses.create_params
|
assert "temperature" not in fake_client.responses.create_params
|
||||||
|
|
||||||
|
|
||||||
|
def test_call_responses_api_sends_structured_text_format() -> None:
|
||||||
|
"""Structured Outputs用のtext.formatをResponses APIへ渡す。"""
|
||||||
|
fake_client = FakeClient(FakeResponse(output_text='{"schema_version":1,"strategies":[]}'))
|
||||||
|
response_text_format = {
|
||||||
|
"type": "json_schema",
|
||||||
|
"name": "entry_candidates",
|
||||||
|
"strict": True,
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": False,
|
||||||
|
"properties": {"schema_version": {"type": "integer"}},
|
||||||
|
"required": ["schema_version"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual = call_responses_api(
|
||||||
|
client=fake_client, # type: ignore[arg-type]
|
||||||
|
model="gpt-5.5-2026-04-23",
|
||||||
|
reasoning_effort="low",
|
||||||
|
system_content="Return JSON.",
|
||||||
|
user_text="Classify.",
|
||||||
|
image_data_urls=[],
|
||||||
|
max_output_tokens=128,
|
||||||
|
response_text_format=response_text_format,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert actual.text.startswith("{")
|
||||||
|
assert fake_client.responses.create_params["text"] == {"format": response_text_format}
|
||||||
|
|
||||||
|
|
||||||
def test_call_responses_api_returns_incomplete_diagnostics_for_empty_text() -> None:
|
def test_call_responses_api_returns_incomplete_diagnostics_for_empty_text() -> None:
|
||||||
"""Empty output_text keeps the API status details for live diagnosis."""
|
"""Empty output_text keeps the API status details for live diagnosis."""
|
||||||
fake_client = FakeClient(
|
fake_client = FakeClient(
|
||||||
@@ -91,3 +121,4 @@ def test_call_responses_api_returns_incomplete_diagnostics_for_empty_text() -> N
|
|||||||
|
|
||||||
assert actual.text == ""
|
assert actual.text == ""
|
||||||
assert "max_output_tokens" in actual.diagnostics.incomplete_details
|
assert "max_output_tokens" in actual.diagnostics.incomplete_details
|
||||||
|
assert not actual.diagnostics.is_completed()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from ea_py.market.target_prices import parse_lines_to_13_allow_subset, sanitize_
|
|||||||
from ea_py.market.target_zones import (
|
from ea_py.market.target_zones import (
|
||||||
build_candidate_id,
|
build_candidate_id,
|
||||||
format_target_zones,
|
format_target_zones,
|
||||||
|
parse_json_to_entry_zones,
|
||||||
parse_lines_to_entry_zones_allow_subset,
|
parse_lines_to_entry_zones_allow_subset,
|
||||||
sanitize_entry_zones,
|
sanitize_entry_zones,
|
||||||
zones_to_numeric_list,
|
zones_to_numeric_list,
|
||||||
@@ -74,6 +75,51 @@ def test_parse_lines_to_entry_zones_allow_subset_valid_zone_returns_zone_values(
|
|||||||
assert actual[2].zone_high == 1898.0
|
assert actual[2].zone_high == 1898.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_json_to_entry_zones_valid_structured_output_returns_zone_values() -> None:
|
||||||
|
"""Structured OutputsのJSON候補を戦略別ゾーンへ展開する。"""
|
||||||
|
actual = parse_json_to_entry_zones(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"strategies": [
|
||||||
|
{
|
||||||
|
"strategy": 2,
|
||||||
|
"decision": "use",
|
||||||
|
"entry": 1895.0,
|
||||||
|
"tp": 1910.0,
|
||||||
|
"sl": 1885.0,
|
||||||
|
"zone_low": 1892.0,
|
||||||
|
"zone_high": 1898.0,
|
||||||
|
"reason_code": "range_edge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"strategy": 4,
|
||||||
|
"decision": "skip",
|
||||||
|
"entry": 0.0,
|
||||||
|
"tp": 0.0,
|
||||||
|
"sl": 0.0,
|
||||||
|
"zone_low": 0.0,
|
||||||
|
"zone_high": 0.0,
|
||||||
|
"reason_code": "skip_mid_range"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
assert actual[2].entry == 1895.0
|
||||||
|
assert actual[2].zone_low == 1892.0
|
||||||
|
assert actual[2].zone_high == 1898.0
|
||||||
|
assert actual[4].entry == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_json_to_entry_zones_invalid_json_returns_stop_values() -> None:
|
||||||
|
"""不正JSONは全停止ゾーンへ倒す。"""
|
||||||
|
actual = parse_json_to_entry_zones("2,1895.00,1910.00,1885.00,1892.00,1898.00")
|
||||||
|
|
||||||
|
assert all(zone.entry == 0.0 for zone in actual.values())
|
||||||
|
|
||||||
|
|
||||||
def test_sanitize_entry_zones_invalid_unselected_strategy_zeroes_it() -> None:
|
def test_sanitize_entry_zones_invalid_unselected_strategy_zeroes_it() -> None:
|
||||||
"""未選択戦略のゾーンは停止値へ補正する。"""
|
"""未選択戦略のゾーンは停止値へ補正する。"""
|
||||||
parsed = parse_lines_to_entry_zones_allow_subset(
|
parsed = parse_lines_to_entry_zones_allow_subset(
|
||||||
@@ -96,6 +142,20 @@ def test_sanitize_entry_zones_allows_near_zone_edge_when_entry_is_farther() -> N
|
|||||||
assert actual[2].entry == 1890.0
|
assert actual[2].entry == 1890.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanitize_entry_zones_rejects_low_reward_risk_ratio() -> None:
|
||||||
|
"""reward/riskが閾値未満の候補は停止値へ補正する。"""
|
||||||
|
parsed = parse_lines_to_entry_zones_allow_subset("2,1895.00,1900.00,1885.00,1892.00,1898.00")
|
||||||
|
|
||||||
|
actual = sanitize_entry_zones(
|
||||||
|
parsed,
|
||||||
|
selected_strategies=[2],
|
||||||
|
current_price=1900.0,
|
||||||
|
min_reward_risk_ratio=1.2,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert actual[2].entry == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_zones_to_numeric_list_valid_zone_returns_legacy_13_values() -> None:
|
def test_zones_to_numeric_list_valid_zone_returns_legacy_13_values() -> None:
|
||||||
"""有効ゾーンのentry/tp/slから既存13行形式を生成する。"""
|
"""有効ゾーンのentry/tp/slから既存13行形式を生成する。"""
|
||||||
parsed = parse_lines_to_entry_zones_allow_subset("4,1910.00,1880.00,1920.00,1908.00,1912.00")
|
parsed = parse_lines_to_entry_zones_allow_subset("4,1910.00,1880.00,1920.00,1908.00,1912.00")
|
||||||
|
|||||||
Reference in New Issue
Block a user