226 lines
5.8 KiB
Python
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 == ""
|