Files
atm-backtesting/tests/test_vision_schema.py

226 lines
5.8 KiB
Python

"""Tests for scripts.vision_schema.M2DExtraction."""
from __future__ import annotations
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
import pytest
from pydantic import ValidationError
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from scripts.vision_schema import ( # noqa: E402
M2DExtraction,
parse_extraction,
parse_extraction_dict,
)
def _buy_payload(**overrides) -> dict:
base = {
"screenshot_file": "dia-1min-example.png",
"data": "2026-05-13",
"ora_utc": "14:23",
"instrument": "DIA",
"directie": "Buy",
"tf_mare": "5min",
"tf_mic": "1min",
"calitate": "Clară",
"entry": 400.0,
"sl": 399.0,
"tp0": 400.5,
"tp1": 401.0,
"tp2": 402.0,
"risc_pct": 0.25,
"outcome_path": "TP0→TP1",
"max_reached": "TP1",
"be_moved": True,
"confidence": "high",
"ambiguities": [],
"note": "",
}
base.update(overrides)
return base
def _sell_payload(**overrides) -> dict:
base = {
"screenshot_file": "dia-sell.png",
"data": "2026-05-13",
"ora_utc": "15:00",
"instrument": "US30",
"directie": "Sell",
"tf_mare": "15min",
"tf_mic": "3min",
"calitate": "Mai mare ca impuls",
"entry": 400.0,
"sl": 401.0,
"tp0": 399.5,
"tp1": 399.0,
"tp2": 398.0,
"risc_pct": 0.3,
"outcome_path": "TP0→TP2",
"max_reached": "TP2",
"be_moved": False,
"confidence": "medium",
"ambiguities": ["entry overlap with wick"],
"note": "nothing",
}
base.update(overrides)
return base
def test_happy_path_buy():
m = parse_extraction_dict(_buy_payload())
assert m.directie == "Buy"
assert m.entry == 400.0
def test_happy_path_sell():
m = parse_extraction_dict(_sell_payload())
assert m.directie == "Sell"
assert m.sl > m.entry > m.tp0 > m.tp1 > m.tp2
def test_parse_extraction_from_json_str():
payload = _buy_payload()
m = parse_extraction(json.dumps(payload))
assert isinstance(m, M2DExtraction)
@pytest.mark.parametrize(
"field,bad_value",
[
("directie", "Long"),
("instrument", "SPY"),
("tf_mare", "30min"),
("tf_mic", "2min"),
("calitate", "Bună"),
("outcome_path", "BE"),
("max_reached", "BE"),
("confidence", "very-high"),
],
)
def test_each_literal_rejection(field, bad_value):
payload = _buy_payload(**{field: bad_value})
with pytest.raises(ValidationError):
parse_extraction_dict(payload)
def test_entry_equals_sl():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(entry=399.0, sl=399.0))
def test_buy_tp_inverted():
# tp1 < tp0 violates ordering
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(tp0=401.0, tp1=400.5, tp2=402.0))
def test_buy_sl_above_entry_rejected():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(sl=400.5))
def test_sell_order_correct():
m = parse_extraction_dict(_sell_payload())
assert m.sl > m.entry
assert m.entry > m.tp0
assert m.tp0 > m.tp1 > m.tp2
def test_sell_order_inverted_rejected():
# using Buy-ordering values with directie=Sell
with pytest.raises(ValidationError):
parse_extraction_dict(
_sell_payload(sl=399.0, entry=400.0, tp0=400.5, tp1=401.0, tp2=402.0)
)
def test_data_in_future():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(data="2099-01-01"))
def test_data_today_ok():
today = datetime.now(timezone.utc).date().isoformat()
m = parse_extraction_dict(_buy_payload(data=today))
assert m.data == today
def test_outcome_path_sl_max_reached_inconsistent():
with pytest.raises(ValidationError):
parse_extraction_dict(
_buy_payload(outcome_path="SL", max_reached="TP1")
)
def test_outcome_path_sl_max_reached_sl_first_ok():
m = parse_extraction_dict(
_buy_payload(outcome_path="SL", max_reached="SL_first")
)
assert m.outcome_path == "SL"
def test_outcome_path_tp0_max_reached_sl_first_rejected():
with pytest.raises(ValidationError):
parse_extraction_dict(
_buy_payload(outcome_path="TP0→TP1", max_reached="SL_first")
)
def test_outcome_path_pending_any_max_reached_ok():
m = parse_extraction_dict(
_buy_payload(outcome_path="pending", max_reached="SL_first")
)
assert m.outcome_path == "pending"
def test_extra_field_forbidden():
payload = _buy_payload()
payload["unexpected_field"] = "x"
with pytest.raises(ValidationError):
parse_extraction_dict(payload)
def test_data_bad_format():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(data="2026/05/13"))
def test_data_bad_format_short():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(data="26-05-13"))
def test_ora_utc_bad_format():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(ora_utc="14:23:00"))
def test_ora_utc_bad_format_no_colon():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(ora_utc="1423"))
def test_ora_utc_invalid_hour():
with pytest.raises(ValidationError):
parse_extraction_dict(_buy_payload(ora_utc="25:00"))
def test_ambiguities_default_empty():
payload = _buy_payload()
del payload["ambiguities"]
m = parse_extraction_dict(payload)
assert m.ambiguities == []
def test_note_default_empty():
payload = _buy_payload()
del payload["note"]
m = parse_extraction_dict(payload)
assert m.note == ""