scripts: pl_calc, vision_schema, calendar_parse + tests (67 passing)
This commit is contained in:
225
tests/test_vision_schema.py
Normal file
225
tests/test_vision_schema.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""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 == ""
|
||||
Reference in New Issue
Block a user