Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ ignore = [
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*" = ["ANN201", "D103", "INP001", "S101"]
"tests/**/*" = ["ANN201", "ANN401", "ARG002", "D103", "INP001", "PLR6301", "S101"]
"src/trade_study/protocols.py" = ["ANN401"]

[tool.ruff.lint.pylint]
Expand Down
88 changes: 86 additions & 2 deletions tests/test_protocols.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Tests for Observable, Tier, Direction, and Annotation (issues #1, #3)."""
"""Tests for protocol types (issues #1, #2, #3)."""

from __future__ import annotations

from typing import Any

import pytest

from trade_study import Direction, Observable, Tier
from trade_study.protocols import Annotation
from trade_study.protocols import Annotation, Scorer, Simulator

# -- Tier enum -----------------------------------------------------------------

Expand Down Expand Up @@ -176,3 +178,85 @@ def test_annotation_resolve_missing_lookup_key() -> None:
ann = Annotation(name="cost", lookup={"a": 1.0}, key="budget")
with pytest.raises(KeyError, match="missing"):
ann.resolve({"budget": "missing"})


# -- Simulator protocol --------------------------------------------------------


class _ToySimulator:
def generate(self, config: dict[str, Any]) -> tuple[Any, Any]:
return config["mu"], config["mu"] + 0.1


class _BadSimulator:
def not_generate(self, config: dict[str, Any]) -> tuple[Any, Any]:
return 0.0, 0.0


def test_simulator_isinstance_conforming() -> None:
assert isinstance(_ToySimulator(), Simulator)


def test_simulator_isinstance_non_conforming() -> None:
assert not isinstance(_BadSimulator(), Simulator)


def test_simulator_generate_returns_tuple() -> None:
sim = _ToySimulator()
result = sim.generate({"mu": 5.0})
assert isinstance(result, tuple)
assert len(result) == 2


def test_simulator_generate_uses_config() -> None:
sim = _ToySimulator()
truth, obs = sim.generate({"mu": 3.0})
assert truth == pytest.approx(3.0)
assert obs == pytest.approx(3.1)


# -- Scorer protocol -----------------------------------------------------------


class _ToyScorer:
def score(
self,
truth: Any,
observations: Any,
config: dict[str, Any],
) -> dict[str, float]:
return {"error": abs(truth - observations)}


class _BadScorer:
def evaluate(self, truth: Any) -> float:
return 0.0


def test_scorer_isinstance_conforming() -> None:
assert isinstance(_ToyScorer(), Scorer)


def test_scorer_isinstance_non_conforming() -> None:
assert not isinstance(_BadScorer(), Scorer)


def test_scorer_returns_dict() -> None:
s = _ToyScorer()
result = s.score(5.0, 5.3, {})
assert isinstance(result, dict)
assert "error" in result


def test_scorer_values_are_float() -> None:
s = _ToyScorer()
result = s.score(5.0, 5.3, {})
assert all(isinstance(v, float) for v in result.values())


def test_simulator_and_scorer_end_to_end() -> None:
sim = _ToySimulator()
scorer = _ToyScorer()
truth, obs = sim.generate({"mu": 2.0})
scores = scorer.score(truth, obs, {"mu": 2.0})
assert scores["error"] == pytest.approx(0.1)
Loading