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
+29
View File
@@ -0,0 +1,29 @@
"""Runtime config loading tests."""
from __future__ import annotations
import pytest
from ea_py.config import load_runtime_config
def test_load_runtime_config_reads_model_and_reasoning_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""OpenAI model and reasoning effort can be overridden without code edits."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.setenv("OPENAI_MODEL", "gpt-test")
monkeypatch.setenv("OPENAI_REASONING_EFFORT", "low")
actual = load_runtime_config()
assert actual.api_key == "test-key"
assert actual.model == "gpt-test"
assert actual.reasoning_effort == "low"
def test_load_runtime_config_rejects_invalid_reasoning_effort(monkeypatch: pytest.MonkeyPatch) -> None:
"""Invalid reasoning effort should fail before an API call is attempted."""
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
monkeypatch.setenv("OPENAI_REASONING_EFFORT", "fastest")
with pytest.raises(RuntimeError, match="OPENAI_REASONING_EFFORT"):
load_runtime_config()
+118
View File
@@ -0,0 +1,118 @@
"""H1インバランス判定のUnit Test。"""
from __future__ import annotations
import pytest
from ea_py.market.imbalance import (
adjust_strategies_for_imbalance,
calculate_average_body_size,
detect_imbalance_signal,
)
from ea_py.types import OhlcBar
def make_bar(open_price: float, close_price: float) -> OhlcBar:
"""テスト用OHLCバーを作る。"""
high = max(open_price, close_price)
low = min(open_price, close_price)
return {
"DateTime": "2026-01-01 00:00",
"Open": open_price,
"High": high,
"Low": low,
"Close": close_price,
}
def test_calculate_average_body_size_excludes_current_bar() -> None:
"""平均実体サイズには判定対象足を含めない。"""
ohlc = [make_bar(100.0, 101.0), make_bar(100.0, 102.0), make_bar(100.0, 110.0)]
actual = calculate_average_body_size(ohlc, current_index=2, period=2)
assert actual == 1.5
@pytest.mark.parametrize(
("open_price", "close_price", "expected"),
[
(100.0, 103.0, "BUY"),
(103.0, 100.0, "SELL"),
(100.0, 101.5, "NONE"),
],
)
def test_detect_imbalance_signal_various_current_bars_returns_expected(
open_price: float,
close_price: float,
expected: str,
) -> None:
"""直近確定足の実体が平均実体の感度倍率を超えた時だけ方向シグナルを返す。"""
ohlc = [make_bar(100.0, 101.0) for _ in range(20)]
ohlc.append(make_bar(open_price, close_price))
actual = detect_imbalance_signal(
ohlc,
avg_body_period=20,
sensitivity=2.0,
min_avg_body_size=0.01,
)
assert actual.signal == expected
def test_detect_imbalance_signal_tiny_average_body_returns_none() -> None:
"""平均実体が極小の場合は誤検知を避ける。"""
ohlc = [make_bar(100.0, 100.0) for _ in range(20)]
ohlc.append(make_bar(100.0, 103.0))
actual = detect_imbalance_signal(
ohlc,
avg_body_period=20,
sensitivity=2.0,
min_avg_body_size=0.01,
)
assert actual.signal == "NONE"
def test_adjust_strategies_for_imbalance_conflicting_up_trend_keeps_stop_strategy() -> None:
"""H4上昇中の売りインバランスは順張りStopだけ残す。"""
ohlc = [make_bar(100.0, 101.0) for _ in range(20)]
ohlc.append(make_bar(103.0, 100.0))
analysis = detect_imbalance_signal(
ohlc,
avg_body_period=20,
sensitivity=2.0,
min_avg_body_size=0.01,
)
actual = adjust_strategies_for_imbalance(
[1, 2],
trend_state=3,
analysis=analysis,
use_filter=True,
)
assert actual == [1]
def test_adjust_strategies_for_imbalance_conflicting_down_trend_keeps_stop_strategy() -> None:
"""H4下降中の買いインバランスは順張りStopだけ残す。"""
ohlc = [make_bar(100.0, 101.0) for _ in range(20)]
ohlc.append(make_bar(100.0, 103.0))
analysis = detect_imbalance_signal(
ohlc,
avg_body_period=20,
sensitivity=2.0,
min_avg_body_size=0.01,
)
actual = adjust_strategies_for_imbalance(
[3, 4],
trend_state=5,
analysis=analysis,
use_filter=True,
)
assert actual == [3]
@@ -0,0 +1,93 @@
"""OpenAI Responses API wrapper tests."""
from __future__ import annotations
from typing import Any
from ea_py.openai_client import call_responses_api
class FakeDumpable:
"""Minimal SDK-like object exposing model_dump."""
def __init__(self, data: dict[str, object]) -> None:
self._data = data
def model_dump(self, *, mode: str, exclude_none: bool) -> dict[str, object]:
"""Return JSON-serializable diagnostic data."""
return self._data
class FakeResponse:
"""Minimal Responses API response shape used by the wrapper."""
def __init__(self, *, output_text: str, incomplete_details: object | None = None) -> None:
self.output_text = output_text
self.model = "gpt-5.5-2026-04-23"
self.status = "completed"
self.incomplete_details = incomplete_details
self.error = None
self.usage = FakeDumpable({"input_tokens": 10, "output_tokens": 1})
class FakeResponsesResource:
"""Capture create parameters and return a fixed response."""
def __init__(self, response: FakeResponse) -> None:
self.response = response
self.create_params: dict[str, Any] = {}
def create(self, **kwargs: Any) -> FakeResponse:
"""Record the request payload."""
self.create_params = kwargs
return self.response
class FakeClient:
"""Minimal OpenAI client shape."""
def __init__(self, response: FakeResponse) -> None:
self.responses = FakeResponsesResource(response)
def test_call_responses_api_sends_reasoning_and_omits_temperature() -> None:
"""GPT-5.5 requests should use reasoning controls instead of temperature."""
fake_client = FakeClient(FakeResponse(output_text=" 0\n"))
actual = call_responses_api(
client=fake_client, # type: ignore[arg-type]
model="gpt-5.5-2026-04-23",
reasoning_effort="none",
system_content="Return one number.",
user_text="Classify.",
image_data_urls=[],
max_output_tokens=128,
)
assert actual.text == "0"
assert fake_client.responses.create_params["reasoning"] == {"effort": "none"}
assert fake_client.responses.create_params["max_output_tokens"] == 128
assert "temperature" not in fake_client.responses.create_params
def test_call_responses_api_returns_incomplete_diagnostics_for_empty_text() -> None:
"""Empty output_text keeps the API status details for live diagnosis."""
fake_client = FakeClient(
FakeResponse(
output_text="",
incomplete_details=FakeDumpable({"reason": "max_output_tokens"}),
)
)
actual = call_responses_api(
client=fake_client, # type: ignore[arg-type]
model="gpt-5.5-2026-04-23",
reasoning_effort="low",
system_content="Return one number.",
user_text="Classify.",
image_data_urls=[],
max_output_tokens=8,
)
assert actual.text == ""
assert "max_output_tokens" in actual.diagnostics.incomplete_details
@@ -0,0 +1,131 @@
"""target_prices変換と戦略選択のUnit Test。"""
from __future__ import annotations
from ea_py.market.target_prices import parse_lines_to_13_allow_subset, sanitize_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,
zones_to_numeric_list,
)
def test_strategies_by_trend_range_state_returns_countertrend_strategies() -> None:
"""レンジstateでは逆張り買いと逆張り売りだけを選択する。"""
actual = strategies_by_trend(0)
assert actual == [2, 4]
def test_parse_lines_to_13_allow_subset_valid_subset_returns_13_values() -> None:
"""GPTの一部戦略行を13値のtarget_prices形式へ展開する。"""
actual = parse_lines_to_13_allow_subset("2,1900.00,1910.00,1890.00\n4,1930.00,1920.00,1940.00")
assert actual == [1, 0.0, 0.0, 0.0, 1900.0, 1910.0, 1890.0, 0.0, 0.0, 0.0, 1930.0, 1920.0, 1940.0]
def test_parse_lines_to_13_allow_subset_no_prices_returns_all_stop_values() -> None:
"""有効価格がない場合は全停止値を返す。"""
actual = parse_lines_to_13_allow_subset("2,0.00,0.00,0.00")
assert actual == [0] * 13
def test_sanitize_numeric_list_invalid_unselected_strategy_zeroes_it() -> None:
"""未選択戦略と価格条件違反の戦略を0.00へ補正する。"""
parsed = [1, 1910.0, 1920.0, 1900.0, 1890.0, 1900.0, 1880.0, 1880.0, 1870.0, 1890.0, 1930.0, 1920.0, 1940.0]
actual = sanitize_numeric_list(parsed, selected_strategies=[2, 4], current_price=1900.0)
assert actual == [1, 0.0, 0.0, 0.0, 1890.0, 1900.0, 1880.0, 0.0, 0.0, 0.0, 1930.0, 1920.0, 1940.0]
def test_sanitize_numeric_list_rejects_far_entry_price() -> None:
"""現在価格から遠すぎる候補は遅延エントリー防止のため0.00へ補正する。"""
parsed = [1, 0.0, 0.0, 0.0, 1870.0, 1900.0, 1860.0, 0.0, 0.0, 0.0, 1910.0, 1880.0, 1920.0]
actual = sanitize_numeric_list(parsed, selected_strategies=[2, 4], current_price=1900.0, max_entry_distance=20.0)
assert actual == [1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1910.0, 1880.0, 1920.0]
def test_sanitize_numeric_list_uses_strategy_specific_entry_distance() -> None:
"""戦略別の距離制限によりStop系だけ広い候補を残せる。"""
parsed = [1, 1925.0, 1940.0, 1915.0, 1875.0, 1900.0, 1865.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
actual = sanitize_numeric_list(
parsed,
selected_strategies=[1, 2],
current_price=1900.0,
max_entry_distance={1: 30.0, 2: 20.0},
)
assert actual == [1, 1925.0, 1940.0, 1915.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
def test_parse_lines_to_entry_zones_allow_subset_valid_zone_returns_zone_values() -> None:
"""GPTのゾーン行を戦略別ゾーンへ展開する。"""
actual = parse_lines_to_entry_zones_allow_subset("2,1895.00,1910.00,1885.00,1892.00,1898.00")
assert actual[2].entry == 1895.0
assert actual[2].zone_low == 1892.0
assert actual[2].zone_high == 1898.0
def test_sanitize_entry_zones_invalid_unselected_strategy_zeroes_it() -> None:
"""未選択戦略のゾーンは停止値へ補正する。"""
parsed = parse_lines_to_entry_zones_allow_subset(
"1,1910.00,1925.00,1900.00,1908.00,1912.00\n"
"2,1895.00,1910.00,1885.00,1892.00,1898.00"
)
actual = sanitize_entry_zones(parsed, selected_strategies=[2], current_price=1900.0)
assert actual[1].entry == 0.0
assert actual[2].entry == 1895.0
def test_sanitize_entry_zones_allows_near_zone_edge_when_entry_is_farther() -> None:
"""ゾーンの最寄り端が近い場合は広めの候補ゾーンを残す。"""
parsed = parse_lines_to_entry_zones_allow_subset("2,1890.00,1910.00,1880.00,1890.00,1896.00")
actual = sanitize_entry_zones(parsed, selected_strategies=[2], current_price=1900.0, max_entry_distance=5.0)
assert actual[2].entry == 1890.0
def test_zones_to_numeric_list_valid_zone_returns_legacy_13_values() -> None:
"""有効ゾーンのentry/tp/slから既存13行形式を生成する。"""
parsed = parse_lines_to_entry_zones_allow_subset("4,1910.00,1880.00,1920.00,1908.00,1912.00")
sanitized = sanitize_entry_zones(parsed, selected_strategies=[4], current_price=1900.0)
actual = zones_to_numeric_list(sanitized)
assert actual == [1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1910.0, 1880.0, 1920.0]
def test_format_target_zones_includes_schema_candidate_and_all_strategies() -> None:
"""MT5向けゾーンファイルはschema/res/candidate/4戦略行で出力する。"""
parsed = parse_lines_to_entry_zones_allow_subset("2,1895.00,1910.00,1885.00,1892.00,1898.00")
sanitized = sanitize_entry_zones(parsed, selected_strategies=[2], current_price=1900.0)
actual = format_target_zones(sanitized, "202605241300")
assert actual.splitlines() == [
"2",
"1",
"202605241300",
"1,0.00,0.00,0.00,0.00",
"2,1892.00,1898.00,1910.00,1885.00",
"3,0.00,0.00,0.00,0.00",
"4,0.00,0.00,0.00,0.00",
]
def test_build_candidate_id_datetime_text_returns_compact_digits() -> None:
"""H1確定足時刻からスペースなしの候補IDを作る。"""
actual = build_candidate_id("2026.05.24 13:00")
assert actual == "202605241300"
@@ -0,0 +1,160 @@
"""market_state分類とH4方向パースのUnit Test。"""
from __future__ import annotations
import math
import pytest
from ea_py.market.trend_state import (
classify_direction_from_ohlc,
classify_market_state,
parse_trend_012_or_none,
)
from ea_py.market.volatility import summarize_ohlc
from ea_py.types import OhlcBar, OhlcSummary
def summary_with(*, eatr_ratio: float, latest_range_to_eatr: float = 1.0, n: int = 20) -> OhlcSummary:
"""market_state分類テスト用のOHLC要約を作る。"""
return {
"n": n,
"high": 10.0,
"low": 1.0,
"range": 9.0,
"eatr": eatr_ratio,
"eatr_baseline": 1.0,
"eatr_ratio": eatr_ratio,
"latest_range": latest_range_to_eatr,
"latest_range_to_eatr": latest_range_to_eatr,
"latest_upper_wick": 0.2,
"latest_lower_wick": 0.2,
"avg_body": 0.5,
"up": 10,
"down": 10,
"slope": 0.1,
}
def linear_ohlc(*, start: float, step: float, count: int = 72) -> list[OhlcBar]:
"""方向判定テスト用の線形OHLCを作る。"""
bars: list[OhlcBar] = []
for index in range(count):
close = start + step * index
open_ = close - step * 0.5
high = max(open_, close) + 0.3
low = min(open_, close) - 0.3
bars.append(
{
"DateTime": str(index),
"Open": open_,
"High": high,
"Low": low,
"Close": close,
}
)
return bars
def oscillating_ohlc(*, center: float = 100.0, count: int = 72) -> list[OhlcBar]:
"""方向判定テスト用の往復的なレンジOHLCを作る。"""
closes = [center + math.sin(index * math.pi / 2.0) for index in range(count)]
bars: list[OhlcBar] = []
previous_close = closes[0]
for index, close in enumerate(closes):
open_ = previous_close
high = max(open_, close) + 0.8
low = min(open_, close) - 0.8
bars.append(
{
"DateTime": str(index),
"Open": open_,
"High": high,
"Low": low,
"Close": close,
}
)
previous_close = close
return bars
@pytest.mark.parametrize(
("text", "expected"),
[
("0", 0),
("answer: 1", 1),
("2\nreason", 2),
("3", None),
("", None),
],
)
def test_parse_trend_012_or_none_various_inputs_returns_expected(text: str, expected: int | None) -> None:
"""GPT方向出力から0/1/2だけを許可して抽出する。"""
actual = parse_trend_012_or_none(text)
assert actual == expected
def test_classify_direction_from_ohlc_clear_uptrend_returns_up() -> None:
"""明確なH4上昇は方向1を返す。"""
ohlc = linear_ohlc(start=100.0, step=1.0)
actual, _reason = classify_direction_from_ohlc(ohlc, summarize_ohlc(ohlc))
assert actual == 1
def test_classify_direction_from_ohlc_clear_downtrend_returns_down() -> None:
"""明確なH4下降は方向2を返す。"""
ohlc = linear_ohlc(start=170.0, step=-1.0)
actual, _reason = classify_direction_from_ohlc(ohlc, summarize_ohlc(ohlc))
assert actual == 2
def test_classify_direction_from_ohlc_choppy_range_returns_range() -> None:
"""往復的なH4レンジは方向0へ倒す。"""
ohlc = oscillating_ohlc()
actual, _reason = classify_direction_from_ohlc(ohlc, summarize_ohlc(ohlc))
assert actual == 0
@pytest.mark.parametrize(
("direction", "eatr_ratio", "expected"),
[
(0, 1.0, 0),
(0, 1.2, 1),
(1, 1.0, 2),
(1, 1.2, 3),
(2, 1.0, 4),
(2, 1.2, 5),
],
)
def test_classify_market_state_direction_and_volatility_returns_expected(
direction: int,
eatr_ratio: float,
expected: int,
) -> None:
"""方向判定とボラティリティ比から期待するmarket_stateへ分類する。"""
actual, _reason = classify_market_state(direction, summary_with(eatr_ratio=eatr_ratio))
assert actual == expected
def test_classify_market_state_extreme_volatility_returns_high_vol_state() -> None:
"""旧異常ボラティリティ閾値以上でも停止せず高ボラstateへ吸収する。"""
actual, _reason = classify_market_state(1, summary_with(eatr_ratio=2.0))
assert actual == 3
def test_classify_market_state_extreme_latest_range_returns_high_vol_state() -> None:
"""直近足レンジが旧異常閾値以上でも方向に応じた高ボラstateを返す。"""
actual, _reason = classify_market_state(2, summary_with(eatr_ratio=1.0, latest_range_to_eatr=2.5))
assert actual == 5
@@ -0,0 +1,37 @@
"""ボラティリティ計算のUnit Test。"""
from __future__ import annotations
from ea_py.market.volatility import calc_exponential_atr_values, calc_true_ranges, summarize_ohlc
from ea_py.types import OhlcBar
def test_calc_true_ranges_uses_previous_close_for_gap_range() -> None:
"""ギャップがある足では前回終値との差をTrue Rangeに反映する。"""
ohlc: list[OhlcBar] = [
{"DateTime": "1", "Open": 10.0, "High": 12.0, "Low": 9.0, "Close": 11.0},
{"DateTime": "2", "Open": 15.0, "High": 16.0, "Low": 14.0, "Close": 15.0},
]
actual = calc_true_ranges(ohlc)
assert actual == [3.0, 5.0]
def test_calc_exponential_atr_values_short_series_returns_seed_for_all_values() -> None:
"""期間未満のTR列では平均値を全要素のEATRとして返す。"""
actual = calc_exponential_atr_values([2.0, 4.0], period=14)
assert actual == [3.0, 3.0]
def test_summarize_ohlc_slope_uses_weighted_close() -> None:
"""傾きはClose単体ではなくWeighted Closeの始点/終点差から計算する。"""
ohlc: list[OhlcBar] = [
{"DateTime": "1", "Open": 10.0, "High": 20.0, "Low": 10.0, "Close": 10.0},
{"DateTime": "2", "Open": 10.0, "High": 30.0, "Low": 10.0, "Close": 10.0},
]
actual = summarize_ohlc(ohlc)
assert actual["slope"] == 2.5