Skip to content
Open
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
16 changes: 16 additions & 0 deletions autopredict/domains/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@
)
from autopredict.domains.registry import DomainRegistry, domain_registry
from autopredict.domains.router import RoutedSpecialistStrategy
from autopredict.domains.sports import (
SportsDomainAdapter,
TennisSpecialistStrategy,
build_default_sports_model,
sports_calibration_examples,
sports_dataset,
sports_evaluation_examples,
sports_training_examples,
)
from autopredict.domains.weather import (
WeatherDomainAdapter,
WeatherSpecialistStrategy,
Expand Down Expand Up @@ -82,11 +91,14 @@
"QuestionConditionedExample",
"QuestionConditionedLinearModel",
"SpecialistOrderPolicy",
"SportsDomainAdapter",
"TennisSpecialistStrategy",
"WeatherDomainAdapter",
"WeatherSpecialistStrategy",
"build_default_finance_model",
"build_default_generic_model",
"build_default_politics_model",
"build_default_sports_model",
"build_default_weather_model",
"build_domain_report_card",
"domain_registry",
Expand All @@ -103,6 +115,10 @@
"politics_dataset",
"politics_evaluation_examples",
"politics_training_examples",
"sports_calibration_examples",
"sports_dataset",
"sports_evaluation_examples",
"sports_training_examples",
"weather_calibration_examples",
"weather_dataset",
"weather_evaluation_examples",
Expand Down
6 changes: 5 additions & 1 deletion autopredict/domains/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
from autopredict.domains.finance import FinanceSpecialistStrategy
from autopredict.domains.generic import GenericSpecialistStrategy
from autopredict.domains.politics import PoliticsSpecialistStrategy
from autopredict.domains.sports import TennisSpecialistStrategy
from autopredict.domains.weather import WeatherSpecialistStrategy
from autopredict.prediction_market.types import MarketSignal, MarketSnapshot, StrategyContext


class RoutedSpecialistStrategy:
"""Route markets to finance, politics, or generic specialist strategies."""
"""Route markets to finance, politics, weather, sports, or generic strategies."""

name = "routed_specialist"

Expand All @@ -20,6 +21,7 @@ def __init__(self, policy: SpecialistOrderPolicy | None = None) -> None:
self.finance = FinanceSpecialistStrategy(policy=self.policy)
self.politics = PoliticsSpecialistStrategy(policy=self.policy)
self.weather = WeatherSpecialistStrategy(policy=self.policy)
self.sports = TennisSpecialistStrategy(policy=self.policy)
self.generic = GenericSpecialistStrategy(policy=self.policy)

def generate_signal(
Expand Down Expand Up @@ -48,4 +50,6 @@ def _select_strategy(self, snapshot: MarketSnapshot):
return self.politics
if domain == "weather":
return self.weather
if domain == "sports" or category == "sports":
return self.sports
return self.generic
21 changes: 21 additions & 0 deletions autopredict/domains/sports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Sports (live tennis) domain adapters."""

from autopredict.domains.sports.adapter import SportsDomainAdapter
from autopredict.domains.sports.model import (
build_default_sports_model,
sports_calibration_examples,
sports_dataset,
sports_evaluation_examples,
sports_training_examples,
)
from autopredict.domains.sports.strategy import TennisSpecialistStrategy

__all__ = [
"SportsDomainAdapter",
"TennisSpecialistStrategy",
"build_default_sports_model",
"sports_calibration_examples",
"sports_dataset",
"sports_evaluation_examples",
"sports_training_examples",
]
46 changes: 46 additions & 0 deletions autopredict/domains/sports/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Sports (live tennis) domain adapter for caller-provided match state.

Vendor note: this domain is contributed by the Live Tennis API team
(https://livetennisapi.com). It publishes live tennis match state (score,
server, break point, retirement/walkover/completed status) as an AutoPredict
domain so a strategy trading tennis EVENT MARKETS on the existing venue clients
can subscribe to it. It is a data input, not a market or execution venue; judge
accordingly.
"""

from __future__ import annotations

from autopredict.domains.base import DomainFeatureBundle
from autopredict.ingestion.base import IngestionBatch
from autopredict.ingestion.sports.features import build_sports_features


class SportsDomainAdapter:
"""Build a normalized tennis bundle from explicit match-state batches."""

name = "sports"

def __init__(self, *, match_state_batch: IngestionBatch) -> None:
self.match_state_batch = match_state_batch

@classmethod
def from_batches(cls, *, match_state_batch: IngestionBatch) -> "SportsDomainAdapter":
"""Return an adapter over an observed tennis match-state batch."""

return cls(match_state_batch=match_state_batch)

def build_bundle(self) -> DomainFeatureBundle:
match_state_batch = self.match_state_batch
features = build_sports_features(match_state_batch)
dominant_record = match_state_batch.evidence[-1]
return DomainFeatureBundle(
domain="sports",
features=features,
metadata={
"domain": "sports",
"market_family": str(dominant_record.metadata.get("market_family", "tennis")),
"regime": str(dominant_record.metadata.get("regime", "in_play")),
"feature_version": "sports.phase1",
},
evidence_ids=match_state_batch.record_ids,
)
55 changes: 55 additions & 0 deletions autopredict/domains/sports/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Default sports (tennis) model.

AutoPredict does not bundle offline tennis examples as product data. The default
model is a neutral market-implied fallback until a verified model is configured
explicitly, so a packaged example never masquerades as production alpha. The
Live Tennis API supplies live match STATE; it does not ship a proven fair-value
model, which is exactly why the neutral no-edge default is the honest one here.
"""

from __future__ import annotations

from functools import lru_cache

from autopredict.domains.modeling import (
MarketImpliedNoEdgeModel,
QuestionConditionedDataset,
QuestionConditionedExample,
)


@lru_cache(maxsize=1)
def sports_dataset() -> QuestionConditionedDataset:
"""Return the configured sports dataset metadata."""

return QuestionConditionedDataset(
name="no_verified_sports_dataset",
version="none",
domain="sports",
examples_by_split={},
)


def sports_training_examples() -> tuple[QuestionConditionedExample, ...]:
"""Return offline training examples for sports."""

return sports_dataset().split_examples("train")


def sports_calibration_examples() -> tuple[QuestionConditionedExample, ...]:
"""Return held-out calibration examples for sports."""

return sports_dataset().split_examples("calibration")


def sports_evaluation_examples() -> tuple[QuestionConditionedExample, ...]:
"""Return held-out evaluation examples for sports."""

return sports_dataset().split_examples("evaluation")


@lru_cache(maxsize=1)
def build_default_sports_model() -> MarketImpliedNoEdgeModel:
"""Return the production-safe neutral sports model."""

return MarketImpliedNoEdgeModel("sports_market_implied_no_edge", "sports")
79 changes: 79 additions & 0 deletions autopredict/domains/sports/strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Sports specialist strategy backed by a question-conditioned model."""

from __future__ import annotations

from autopredict.domains.base import (
SpecialistOrderPolicy,
build_single_edge_order,
snapshot_label,
)
from autopredict.domains.modeling import QuestionConditionedLinearModel
from autopredict.domains.sports.model import build_default_sports_model
from autopredict.prediction_market.types import MarketSignal, MarketSnapshot, StrategyContext


class TennisSpecialistStrategy:
"""Model-backed tennis strategy driven by question and match-state features."""

name = "tennis_specialist"

def __init__(
self,
policy: SpecialistOrderPolicy | None = None,
model: QuestionConditionedLinearModel | None = None,
) -> None:
self.policy = policy or SpecialistOrderPolicy(
min_abs_edge=0.02,
max_bankroll_fraction=0.05,
aggressive_edge=0.06,
urgency_regimes=("retirement", "in_play"),
)
self.model = model or build_default_sports_model()

def generate_signal(
self,
snapshot: MarketSnapshot,
context: StrategyContext,
) -> MarketSignal | None:
del context
if snapshot_label(snapshot, "domain", "") != "sports":
return None

family = snapshot_label(snapshot, "market_family", "tennis")
regime = snapshot_label(snapshot, "regime", "in_play")
prediction = self.model.predict(
snapshot.market.question,
{
**snapshot.features,
"market_prob": snapshot.market.market_prob,
"spread_bps": snapshot.market.spread_bps,
"total_liquidity": snapshot.market.total_liquidity,
},
snapshot.labels,
)
return MarketSignal(
fair_prob=prediction.probability,
confidence=prediction.confidence,
rationale=prediction.rationale,
tags=("domain", "sports", "model", family, regime),
metadata={
**prediction.metadata,
"domain": "sports",
"market_family": family,
"regime": regime,
},
)

def build_orders(
self,
snapshot: MarketSnapshot,
signal: MarketSignal,
context: StrategyContext,
) -> list:
return build_single_edge_order(
snapshot,
signal,
context,
strategy_name=self.name,
policy=self.policy,
)
17 changes: 17 additions & 0 deletions autopredict/ingestion/sports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Sports (live tennis) ingestion helpers."""

from autopredict.ingestion.sports.features import build_sports_features
from autopredict.ingestion.sports.match_state import (
MATCH_STATE_SOURCE,
build_match_state_row,
derive_break_point,
normalize_match_states,
)

__all__ = [
"MATCH_STATE_SOURCE",
"build_match_state_row",
"build_sports_features",
"derive_break_point",
"normalize_match_states",
]
36 changes: 36 additions & 0 deletions autopredict/ingestion/sports/features.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Feature builders for live tennis match-state evidence."""

from __future__ import annotations

from typing import Any

from autopredict.ingestion.base import IngestionBatch


def build_sports_features(match_state_batch: IngestionBatch) -> dict[str, Any]:
"""Build a small deterministic tennis match-state feature payload."""

records = match_state_batch.evidence
win_probabilities = [
float(record.payload["win_probability_p1"])
for record in records
if record.payload.get("win_probability_p1") is not None
]
num_break_points = sum(1 for record in records if bool(record.payload.get("break_point")))
num_tiebreaks = sum(1 for record in records if bool(record.payload.get("is_tiebreak")))
num_live = sum(1 for record in records if record.payload.get("status") == "live")
num_completed = sum(1 for record in records if record.payload.get("status") == "completed")
num_retired = sum(
1 for record in records if record.payload.get("event_status") in ("Retired", "Walk Over")
)
return {
"num_matches": len(records),
"num_live": num_live,
"num_completed": num_completed,
"num_retired": num_retired,
"num_break_points": num_break_points,
"has_break_point": num_break_points > 0,
"num_tiebreaks": num_tiebreaks,
"max_win_probability_p1": max(win_probabilities) if win_probabilities else 0.0,
"has_win_probability": bool(win_probabilities),
}
Loading