refactor resolvers
This commit is contained in:
@@ -5,6 +5,60 @@ screenshotter:
|
|||||||
|
|
||||||
resolver:
|
resolver:
|
||||||
api_key: 733f6882605be2de8980bbd074091ee4
|
api_key: 733f6882605be2de8980bbd074091ee4
|
||||||
|
league_map:
|
||||||
|
# European cups
|
||||||
|
liga mistrů: 2
|
||||||
|
champions league: 2
|
||||||
|
evropská liga: 3
|
||||||
|
europa league: 3
|
||||||
|
konferenční liga: 848
|
||||||
|
conference league: 848
|
||||||
|
# Top flights
|
||||||
|
1. anglie: 39
|
||||||
|
1. belgie: 144
|
||||||
|
1. česko: 345
|
||||||
|
1. dánsko: 119
|
||||||
|
1. francie: 61
|
||||||
|
1. itálie: 135
|
||||||
|
1. itálie - ženy: 794
|
||||||
|
1. německo: 78
|
||||||
|
1. nizozemsko: 88
|
||||||
|
1. polsko: 106
|
||||||
|
1. portugalsko: 94
|
||||||
|
1. rakousko: 218
|
||||||
|
1. rumunsko: 283
|
||||||
|
1. skotsko: 179
|
||||||
|
1. slovensko: 332
|
||||||
|
1. španělsko: 140
|
||||||
|
1. wales: 771
|
||||||
|
# Second divisions
|
||||||
|
2. anglie: 40
|
||||||
|
2. česko: 346
|
||||||
|
2. francie: 62
|
||||||
|
2. itálie: 136
|
||||||
|
2. německo: 79
|
||||||
|
2. nizozemsko: 89
|
||||||
|
2. rakousko: 219
|
||||||
|
2. slovensko: 333
|
||||||
|
2. španělsko: 141
|
||||||
|
# Third divisions
|
||||||
|
3. francie: 63
|
||||||
|
3. česko msfl: 349
|
||||||
|
3. česko čfl: 348
|
||||||
|
# Fourth divisions
|
||||||
|
4. česko - sk. a: 350
|
||||||
|
4. česko - sk. b: 351
|
||||||
|
4. česko - sk. c: 352
|
||||||
|
4. česko - sk. d: 353
|
||||||
|
4. česko - sk. e: 354
|
||||||
|
4. česko - sk. f: 686
|
||||||
|
# Women
|
||||||
|
1. česko - ženy: 669
|
||||||
|
fortuna=liga ženy: 669
|
||||||
|
# Domestic cups
|
||||||
|
anglie - fa cup: 45
|
||||||
|
anglie - efl cup: 48
|
||||||
|
česko - pohár: 347
|
||||||
|
|
||||||
img_classifier:
|
img_classifier:
|
||||||
target_path: data/screenshots/
|
target_path: data/screenshots/
|
||||||
|
|||||||
@@ -3,18 +3,24 @@ import argparse
|
|||||||
import yaml
|
import yaml
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from beaky import _ansi
|
||||||
from beaky.config import Config
|
from beaky.config import Config
|
||||||
from beaky.link_classifier.classifier import LinkClassifier
|
from beaky.link_classifier.classifier import LinkClassifier
|
||||||
from beaky.resolvers.resolver import _B, _GRAY, _GREEN, _R, _RED, _YELLOW, TicketResolver, TicketVerdict
|
from beaky.resolvers.resolver import TicketResolver, TicketVerdict
|
||||||
from beaky.scanner.scanner import Links
|
from beaky.scanner.scanner import Links
|
||||||
from beaky.screenshotter.screenshotter import Screenshotter
|
from beaky.screenshotter.screenshotter import Screenshotter
|
||||||
|
|
||||||
_VERDICT_COLOR = {
|
|
||||||
TicketVerdict.TRUTHFUL: _GREEN,
|
def _verdict_str(verdict: TicketVerdict) -> str:
|
||||||
TicketVerdict.NOT_TRUTHFUL: _RED,
|
text = f"VERDICT: {verdict.value.upper()}"
|
||||||
TicketVerdict.POSSIBLY_TRUTHFUL: _YELLOW,
|
if verdict == TicketVerdict.TRUTHFUL:
|
||||||
TicketVerdict.UNKNOWN: _GRAY,
|
return _ansi.green(text)
|
||||||
}
|
if verdict == TicketVerdict.NOT_TRUTHFUL:
|
||||||
|
return _ansi.red(text)
|
||||||
|
if verdict == TicketVerdict.POSSIBLY_TRUTHFUL:
|
||||||
|
return _ansi.yellow(text)
|
||||||
|
return _ansi.gray(text)
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str) -> Config | None:
|
def load_config(path: str) -> Config | None:
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
@@ -81,8 +87,7 @@ def main() -> None:
|
|||||||
|
|
||||||
print(f"\n--- Resolving ticket {link.id} ---")
|
print(f"\n--- Resolving ticket {link.id} ---")
|
||||||
resolved = resolver.resolve(ticket)
|
resolved = resolver.resolve(ticket)
|
||||||
color = _VERDICT_COLOR.get(resolved.verdict, "")
|
print(f"\n {_ansi.bold(_verdict_str(resolved.verdict))}")
|
||||||
print(f"\n {color}{_B}VERDICT: {resolved.verdict.value.upper()}{_R}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -15,7 +15,23 @@ class BetType(str, Enum):
|
|||||||
GOAL_AMOUNT = "goal_amount"
|
GOAL_AMOUNT = "goal_amount"
|
||||||
GOAL_HANDICAP = "goal_handicap"
|
GOAL_HANDICAP = "goal_handicap"
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
...
|
|
||||||
|
|
||||||
|
class BetOutcome(str, Enum):
|
||||||
|
WIN = "win"
|
||||||
|
LOSE = "lose"
|
||||||
|
VOID = "void" # stake returned (e.g. WinLose on draw, integer goal line hit)
|
||||||
|
UNKNOWN = "unknown" # fixture not found or unclassified bet
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MatchInfo:
|
||||||
|
goals_home: int
|
||||||
|
goals_away: int
|
||||||
|
half_time_home: int | None = None
|
||||||
|
half_time_away: int | None = None
|
||||||
|
corners_home: int | None = None
|
||||||
|
corners_away: int | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -25,67 +41,102 @@ class Bet(ABC):
|
|||||||
team2Name: str
|
team2Name: str
|
||||||
date: datetime
|
date: datetime
|
||||||
league: str
|
league: str
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def resolve(self) -> None: pass
|
def resolve(self, match: MatchInfo) -> BetOutcome: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WinDrawLose(Bet):
|
class WinDrawLose(Bet):
|
||||||
"""Výsledek zápasu 1X2"""
|
"""Výsledek zápasu 1X2"""
|
||||||
betType: Literal["X", "0", "1", "2"] = "0"
|
betType: Literal["X", "0", "1", "2"] = "0"
|
||||||
def resolve(self) -> None:
|
|
||||||
...
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
home, away = match.goals_home, match.goals_away
|
||||||
|
bet_draw = self.betType in ("X", "0")
|
||||||
|
if bet_draw:
|
||||||
|
return BetOutcome.WIN if home == away else BetOutcome.LOSE
|
||||||
|
actual = "1" if home > away else ("0" if home == away else "2")
|
||||||
|
return BetOutcome.WIN if actual == self.betType else BetOutcome.LOSE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Advance(Bet):
|
class Advance(Bet):
|
||||||
"""What team advances to next round"""
|
"""What team advances to next round"""
|
||||||
def resolve(self) -> None:
|
|
||||||
raise NotImplementedError("Vyser si voko vine")
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
raise NotImplementedError("Advance bet resolution is not implemented")
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WinDrawLoseDouble(Bet):
|
class WinDrawLoseDouble(Bet):
|
||||||
"""Výsledek zápasu - double"""
|
"""Výsledek zápasu - double"""
|
||||||
betType: Literal["01", "12", "02"] = "01"
|
betType: Literal["01", "12", "02"] = "01"
|
||||||
def resolve(self) -> None:
|
|
||||||
...
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
home, away = match.goals_home, match.goals_away
|
||||||
|
actual = "1" if home > away else ("0" if home == away else "2")
|
||||||
|
return BetOutcome.WIN if actual in self.betType else BetOutcome.LOSE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WinLose(Bet):
|
class WinLose(Bet):
|
||||||
"""Výsledek zápasu bez remízy"""
|
"""Výsledek zápasu bez remízy"""
|
||||||
betType: Literal["1", "2"] = "1"
|
betType: Literal["1", "2"] = "1"
|
||||||
def resolve(self) -> None:
|
|
||||||
...
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
home, away = match.goals_home, match.goals_away
|
||||||
|
if home == away:
|
||||||
|
return BetOutcome.VOID
|
||||||
|
actual = "1" if home > away else "2"
|
||||||
|
return BetOutcome.WIN if actual == self.betType else BetOutcome.LOSE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BothTeamScored(Bet):
|
class BothTeamScored(Bet):
|
||||||
def resolve(self) -> None:
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
...
|
return BetOutcome.WIN if match.goals_home > 0 and match.goals_away > 0 else BetOutcome.LOSE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GoalAmount(Bet):
|
class GoalAmount(Bet):
|
||||||
"""Počet gólů v zápasu — over/under total goals"""
|
"""Počet gólů v zápasu — over/under total goals"""
|
||||||
line: float = 0.0 # goal line, e.g. 2.5
|
line: float = 0.0 # goal line, e.g. 2.5
|
||||||
over: bool = True # True = more than line, False = less than line
|
over: bool = True # True = more than line, False = less than line
|
||||||
def resolve(self) -> None:
|
|
||||||
...
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
total = match.goals_home + match.goals_away
|
||||||
|
if total == self.line:
|
||||||
|
return BetOutcome.VOID
|
||||||
|
won = total > self.line if self.over else total < self.line
|
||||||
|
return BetOutcome.WIN if won else BetOutcome.LOSE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GoalHandicap(Bet):
|
class GoalHandicap(Bet):
|
||||||
"""Goal handicap for a specific team — add handicap_amount to team's score, team wins = you win"""
|
"""Goal handicap for a specific team — add handicap_amount to team's score, team wins = you win"""
|
||||||
team_bet: Literal["1", "2"] = "1" # which team the handicap is applied to
|
team_bet: Literal["1", "2"] = "1" # which team the handicap is applied to
|
||||||
handicap_amount: float = 0.0 # e.g. +1.5 or -0.5
|
handicap_amount: float = 0.0 # e.g. +1.5 or -0.5
|
||||||
def resolve(self) -> None:
|
|
||||||
...
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
home = match.goals_home + (self.handicap_amount if self.team_bet == "1" else 0.0)
|
||||||
|
away = match.goals_away + (self.handicap_amount if self.team_bet == "2" else 0.0)
|
||||||
|
if home == away:
|
||||||
|
return BetOutcome.VOID
|
||||||
|
actual_winner = "1" if home > away else "2"
|
||||||
|
return BetOutcome.WIN if actual_winner == self.team_bet else BetOutcome.LOSE
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UnknownTicket(Bet):
|
class UnknownTicket(Bet):
|
||||||
"""Bet type that could not be classified"""
|
"""Bet type that could not be classified"""
|
||||||
raw_text: str = ""
|
raw_text: str = ""
|
||||||
def resolve(self) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
def resolve(self, match: MatchInfo) -> BetOutcome:
|
||||||
|
return BetOutcome.UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Ticket:
|
class Ticket:
|
||||||
id: int
|
id: int
|
||||||
bets: list[Bet]
|
bets: list[Bet]
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ from pydantic.dataclasses import dataclass
|
|||||||
@dataclass
|
@dataclass
|
||||||
class ResolverConfig:
|
class ResolverConfig:
|
||||||
api_key: str
|
api_key: str
|
||||||
|
league_map: dict[str, int]
|
||||||
|
|||||||
@@ -7,97 +7,20 @@ from typing import Any
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from beaky import _ansi
|
||||||
from beaky.datamodels.ticket import (
|
from beaky.datamodels.ticket import (
|
||||||
Bet,
|
Bet,
|
||||||
BothTeamScored,
|
BetOutcome,
|
||||||
GoalAmount,
|
MatchInfo,
|
||||||
GoalHandicap,
|
|
||||||
Ticket,
|
Ticket,
|
||||||
UnknownTicket,
|
UnknownTicket,
|
||||||
WinDrawLose,
|
|
||||||
WinDrawLoseDouble,
|
|
||||||
WinLose,
|
|
||||||
)
|
)
|
||||||
from beaky.resolvers.config import ResolverConfig
|
from beaky.resolvers.config import ResolverConfig
|
||||||
|
|
||||||
_API_BASE = "https://v3.football.api-sports.io"
|
_API_BASE = "https://v3.football.api-sports.io"
|
||||||
|
|
||||||
# Fortuna league strings (lowercased substring match) -> api-football league ID
|
|
||||||
_LEAGUE_MAP: dict[str, int] = {
|
|
||||||
# European cups
|
|
||||||
"liga mistrů": 2,
|
|
||||||
"champions league": 2,
|
|
||||||
"evropská liga": 3,
|
|
||||||
"europa league": 3,
|
|
||||||
"konferenční liga": 848,
|
|
||||||
"conference league": 848,
|
|
||||||
# Top flights
|
|
||||||
"1. anglie": 39,
|
|
||||||
"1. belgie": 144,
|
|
||||||
"1. česko": 345,
|
|
||||||
"1. dánsko": 119,
|
|
||||||
"1. francie": 61,
|
|
||||||
"1. itálie": 135,
|
|
||||||
"1. itálie - ženy": 794,
|
|
||||||
"1. německo": 78,
|
|
||||||
"1. nizozemsko": 88,
|
|
||||||
"1. polsko": 106,
|
|
||||||
"1. portugalsko": 94,
|
|
||||||
"1. rakousko": 218,
|
|
||||||
"1. rumunsko": 283,
|
|
||||||
"1. skotsko": 179,
|
|
||||||
"1. slovensko": 332,
|
|
||||||
"1. španělsko": 140,
|
|
||||||
"1. wales": 771,
|
|
||||||
# Second divisions
|
|
||||||
"2. anglie": 40,
|
|
||||||
"2. česko": 346,
|
|
||||||
"2. francie": 62,
|
|
||||||
"2. itálie": 136,
|
|
||||||
"2. německo": 79,
|
|
||||||
"2. nizozemsko": 89,
|
|
||||||
"2. rakousko": 219,
|
|
||||||
"2. slovensko": 333,
|
|
||||||
"2. španělsko": 141,
|
|
||||||
# Third divisions
|
|
||||||
"3. francie": 63,
|
|
||||||
"3. česko msfl": 349,
|
|
||||||
"3. česko čfl": 348,
|
|
||||||
# Fourth divisions
|
|
||||||
"4. česko - sk. a": 350,
|
|
||||||
"4. česko - sk. b": 351,
|
|
||||||
"4. česko - sk. c": 352,
|
|
||||||
"4. česko - sk. d": 353,
|
|
||||||
"4. česko - sk. e": 354,
|
|
||||||
"4. česko - sk. f": 686,
|
|
||||||
# Women
|
|
||||||
"1. česko - ženy": 669,
|
|
||||||
"fortuna=liga ženy": 669,
|
|
||||||
# Domestic cups
|
|
||||||
"anglie - fa cup": 45,
|
|
||||||
"anglie - efl cup": 48,
|
|
||||||
"česko - pohár": 347,
|
|
||||||
}
|
|
||||||
|
|
||||||
_DATE_WINDOW = 3 # days either side of the bet date to search
|
_DATE_WINDOW = 3 # days either side of the bet date to search
|
||||||
|
|
||||||
# ANSI color helpers
|
|
||||||
_R = "\033[0m"
|
|
||||||
_B = "\033[1m"
|
|
||||||
_DIM= "\033[2m"
|
|
||||||
_GREEN = "\033[32m"
|
|
||||||
_RED = "\033[31m"
|
|
||||||
_YELLOW = "\033[33m"
|
|
||||||
_CYAN = "\033[36m"
|
|
||||||
_GRAY = "\033[90m"
|
|
||||||
|
|
||||||
_OUTCOME_COLOR = {
|
|
||||||
"win": _GREEN,
|
|
||||||
"lose": _RED,
|
|
||||||
"void": _YELLOW,
|
|
||||||
"unknown": _GRAY,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TicketVerdict(str, Enum):
|
class TicketVerdict(str, Enum):
|
||||||
TRUTHFUL = "truthful"
|
TRUTHFUL = "truthful"
|
||||||
@@ -106,13 +29,6 @@ class TicketVerdict(str, Enum):
|
|||||||
UNKNOWN = "unknown — could not resolve enough bets to decide"
|
UNKNOWN = "unknown — could not resolve enough bets to decide"
|
||||||
|
|
||||||
|
|
||||||
class BetOutcome(str, Enum):
|
|
||||||
WIN = "win"
|
|
||||||
LOSE = "lose"
|
|
||||||
VOID = "void" # stake returned (e.g. WinLose on draw, integer goal line hit)
|
|
||||||
UNKNOWN = "unknown" # fixture not found or unclassified bet
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ResolvedBet:
|
class ResolvedBet:
|
||||||
bet: Bet
|
bet: Bet
|
||||||
@@ -166,6 +82,7 @@ def _get(url: str, headers: dict[str, str], params: dict[str, str | int], retrie
|
|||||||
class TicketResolver:
|
class TicketResolver:
|
||||||
def __init__(self, config: ResolverConfig):
|
def __init__(self, config: ResolverConfig):
|
||||||
self._headers = {"x-apisports-key": config.api_key}
|
self._headers = {"x-apisports-key": config.api_key}
|
||||||
|
self._league_map = config.league_map
|
||||||
# Cache maps (center_date_str, league_id | None) -> list of fixture dicts
|
# Cache maps (center_date_str, league_id | None) -> list of fixture dicts
|
||||||
self._fixture_cache: dict[tuple[str, int | None], list[dict[str, Any]]] = {}
|
self._fixture_cache: dict[tuple[str, int | None], list[dict[str, Any]]] = {}
|
||||||
# Cache maps league name -> (league_id, confidence)
|
# Cache maps league name -> (league_id, confidence)
|
||||||
@@ -179,31 +96,40 @@ class TicketResolver:
|
|||||||
|
|
||||||
def _resolve_bet(self, bet: Bet) -> ResolvedBet:
|
def _resolve_bet(self, bet: Bet) -> ResolvedBet:
|
||||||
bet_type = type(bet).__name__
|
bet_type = type(bet).__name__
|
||||||
print(f"\n {_B}{_CYAN}┌─ [{bet_type}]{_R} {_B}{bet.team1Name} vs {bet.team2Name}{_R}"
|
print(f"\n {_ansi.bold(_ansi.cyan(f'┌─ [{bet_type}]'))} {_ansi.bold(f'{bet.team1Name} vs {bet.team2Name}')}"
|
||||||
f" {_DIM}{bet.date.strftime('%Y-%m-%d')} | {bet.league}{_R}")
|
f" {_ansi.dim(f'{bet.date.strftime('%Y-%m-%d')} | {bet.league}')}")
|
||||||
|
|
||||||
if isinstance(bet, UnknownTicket):
|
if isinstance(bet, UnknownTicket):
|
||||||
print(f" {_GRAY}│ skipping — not implemented: {bet.raw_text!r}{_R}")
|
print(_ansi.gray(f" │ skipping — not implemented: {bet.raw_text!r}"))
|
||||||
print(f" {_GRAY}└─ UNKNOWN{_R}")
|
print(_ansi.gray(" └─ UNKNOWN"))
|
||||||
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN)
|
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN)
|
||||||
|
|
||||||
fixture, name_match, date_prox, league_conf = self._find_fixture(bet)
|
fixture, name_match, date_prox, league_conf = self._find_fixture(bet)
|
||||||
if fixture is None:
|
if fixture is None:
|
||||||
print(f" {_GRAY}└─ UNKNOWN — no fixture found{_R}")
|
print(_ansi.gray(" └─ UNKNOWN — no fixture found"))
|
||||||
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN, league_found=league_conf)
|
return ResolvedBet(bet=bet, outcome=BetOutcome.UNKNOWN, league_found=league_conf)
|
||||||
|
|
||||||
home_name = fixture["teams"]["home"]["name"]
|
home_name = fixture["teams"]["home"]["name"]
|
||||||
away_name = fixture["teams"]["away"]["name"]
|
away_name = fixture["teams"]["away"]["name"]
|
||||||
finished = _is_finished(fixture)
|
finished = _is_finished(fixture)
|
||||||
confidence = round((name_match + date_prox + league_conf + finished) / 4, 3)
|
confidence = round((name_match + date_prox + league_conf + finished) / 4, 3)
|
||||||
outcome = _evaluate_bet(bet, fixture) if finished == 1.0 else BetOutcome.UNKNOWN
|
|
||||||
|
if finished == 1.0:
|
||||||
|
match_info = _fixture_to_match_info(fixture)
|
||||||
|
outcome = bet.resolve(match_info)
|
||||||
|
else:
|
||||||
|
outcome = BetOutcome.UNKNOWN
|
||||||
|
|
||||||
goals = fixture["goals"]
|
goals = fixture["goals"]
|
||||||
color = _OUTCOME_COLOR.get(outcome.value, _GRAY)
|
print(_ansi.dim(
|
||||||
print(f" {_DIM}│ matched #{fixture['fixture']['id']}: {home_name} vs {away_name}"
|
f" │ matched #{fixture['fixture']['id']}: {home_name} vs {away_name}"
|
||||||
f" | {goals['home']}:{goals['away']} | {fixture['fixture']['status']['short']}"
|
f" | {goals['home']}:{goals['away']} | {fixture['fixture']['status']['short']}"
|
||||||
f" | confidence {confidence} (name={name_match:.2f} date={date_prox:.2f} league={league_conf} finished={finished}){_R}")
|
f" | confidence {confidence} (name={name_match:.2f} date={date_prox:.2f} league={league_conf} finished={finished})"
|
||||||
print(f" {color}{_B}└─ {outcome.value.upper()}{_R}")
|
))
|
||||||
|
print(_ansi.bold(_ansi.green(f" └─ {outcome.value.upper()}") if outcome == BetOutcome.WIN
|
||||||
|
else _ansi.red(f" └─ {outcome.value.upper()}") if outcome == BetOutcome.LOSE
|
||||||
|
else _ansi.yellow(f" └─ {outcome.value.upper()}") if outcome == BetOutcome.VOID
|
||||||
|
else _ansi.gray(f" └─ {outcome.value.upper()}")))
|
||||||
|
|
||||||
return ResolvedBet(
|
return ResolvedBet(
|
||||||
bet=bet,
|
bet=bet,
|
||||||
@@ -230,13 +156,13 @@ class TicketResolver:
|
|||||||
if league_id is not None:
|
if league_id is not None:
|
||||||
params["league"] = league_id
|
params["league"] = league_id
|
||||||
params["season"] = center.year if center.month >= 7 else center.year - 1
|
params["season"] = center.year if center.month >= 7 else center.year - 1
|
||||||
print(f" {_GRAY}│ GET /fixtures {params}{_R}")
|
print(_ansi.gray(f" │ GET /fixtures {params}"))
|
||||||
resp = _get(f"{_API_BASE}/fixtures", headers=self._headers, params=params)
|
resp = _get(f"{_API_BASE}/fixtures", headers=self._headers, params=params)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
self._fixture_cache[cache_key] = resp.json().get("response", [])
|
self._fixture_cache[cache_key] = resp.json().get("response", [])
|
||||||
print(f" {_GRAY}│ {len(self._fixture_cache[cache_key])} fixtures returned (cached){_R}")
|
print(_ansi.gray(f" │ {len(self._fixture_cache[cache_key])} fixtures returned (cached)"))
|
||||||
else:
|
else:
|
||||||
print(f" {_GRAY}│ /fixtures (±{_DATE_WINDOW}d of {date_str}, league={league_id}) served from cache{_R}")
|
print(_ansi.gray(f" │ /fixtures (±{_DATE_WINDOW}d of {date_str}, league={league_id}) served from cache"))
|
||||||
|
|
||||||
fixture, name_match, date_prox = _best_fixture_match(
|
fixture, name_match, date_prox = _best_fixture_match(
|
||||||
self._fixture_cache[cache_key], bet.team1Name, bet.team2Name, center
|
self._fixture_cache[cache_key], bet.team1Name, bet.team2Name, center
|
||||||
@@ -248,28 +174,57 @@ class TicketResolver:
|
|||||||
if key in self._league_cache:
|
if key in self._league_cache:
|
||||||
return self._league_cache[key]
|
return self._league_cache[key]
|
||||||
|
|
||||||
for pattern, league_id in _LEAGUE_MAP.items():
|
for pattern, league_id in self._league_map.items():
|
||||||
if pattern in key:
|
if pattern in key:
|
||||||
print(f" {_GRAY}│ league {league_name!r} -> id={league_id} (static map){_R}")
|
print(_ansi.gray(f" │ league {league_name!r} -> id={league_id} (static map)"))
|
||||||
self._league_cache[key] = (league_id, 1.0)
|
self._league_cache[key] = (league_id, 1.0)
|
||||||
return league_id, 1.0
|
return league_id, 1.0
|
||||||
|
|
||||||
# Fall back to API search — lower confidence since first result is taken unverified
|
# Fall back to API search — lower confidence since first result is taken unverified
|
||||||
print(f" {_GRAY}│ GET /leagues search={league_name!r}{_R}")
|
print(_ansi.gray(f" │ GET /leagues search={league_name!r}"))
|
||||||
resp = _get(f"{_API_BASE}/leagues", headers=self._headers, params={"search": league_name[:20]})
|
resp = _get(f"{_API_BASE}/leagues", headers=self._headers, params={"search": league_name[:20]})
|
||||||
results = resp.json().get("response", [])
|
results = resp.json().get("response", [])
|
||||||
if results:
|
if results:
|
||||||
league_id = results[0]["league"]["id"]
|
league_id = results[0]["league"]["id"]
|
||||||
league_found_name = results[0]["league"]["name"]
|
league_found_name = results[0]["league"]["name"]
|
||||||
print(f" {_GRAY}│ matched {league_found_name!r} id={league_id} (API fallback, confidence=0.7){_R}")
|
print(_ansi.gray(f" │ matched {league_found_name!r} id={league_id} (API fallback, confidence=0.7)"))
|
||||||
self._league_cache[key] = (league_id, 0.7)
|
self._league_cache[key] = (league_id, 0.7)
|
||||||
return league_id, 0.7
|
return league_id, 0.7
|
||||||
|
|
||||||
print(f" {_GRAY}│ no league found, searching fixtures by date only (confidence=0.3){_R}")
|
print(_ansi.gray(" │ no league found, searching fixtures by date only (confidence=0.3)"))
|
||||||
self._league_cache[key] = (None, 0.3)
|
self._league_cache[key] = (None, 0.3)
|
||||||
return None, 0.3
|
return None, 0.3
|
||||||
|
|
||||||
|
|
||||||
|
def _fixture_to_match_info(fixture: dict[str, Any]) -> MatchInfo:
|
||||||
|
goals = fixture.get("goals", {})
|
||||||
|
score = fixture.get("score", {})
|
||||||
|
halftime = score.get("halftime", {})
|
||||||
|
|
||||||
|
corners_home: int | None = None
|
||||||
|
corners_away: int | None = None
|
||||||
|
for stat_entry in fixture.get("statistics", []):
|
||||||
|
for stat in stat_entry.get("statistics", []):
|
||||||
|
if stat.get("type") == "Corner Kicks":
|
||||||
|
team_id = stat_entry.get("team", {}).get("id")
|
||||||
|
home_team_id = fixture.get("teams", {}).get("home", {}).get("id")
|
||||||
|
value = stat.get("value")
|
||||||
|
if isinstance(value, int):
|
||||||
|
if team_id == home_team_id:
|
||||||
|
corners_home = value
|
||||||
|
else:
|
||||||
|
corners_away = value
|
||||||
|
|
||||||
|
return MatchInfo(
|
||||||
|
goals_home=goals.get("home", 0),
|
||||||
|
goals_away=goals.get("away", 0),
|
||||||
|
half_time_home=halftime.get("home"),
|
||||||
|
half_time_away=halftime.get("away"),
|
||||||
|
corners_home=corners_home,
|
||||||
|
corners_away=corners_away,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _similarity(a: str, b: str) -> float:
|
def _similarity(a: str, b: str) -> float:
|
||||||
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
return SequenceMatcher(None, a.lower(), b.lower()).ratio()
|
||||||
|
|
||||||
@@ -306,49 +261,3 @@ def _best_fixture_match(fixtures: list[dict[str, Any]], team1: str, team2: str,
|
|||||||
def _is_finished(fixture: dict[str, Any]) -> float:
|
def _is_finished(fixture: dict[str, Any]) -> float:
|
||||||
status = fixture.get("fixture", {}).get("status", {}).get("short", "")
|
status = fixture.get("fixture", {}).get("status", {}).get("short", "")
|
||||||
return 1.0 if status in ("FT", "AET", "PEN", "AWD", "WO") else 0.0
|
return 1.0 if status in ("FT", "AET", "PEN", "AWD", "WO") else 0.0
|
||||||
|
|
||||||
|
|
||||||
def _evaluate_bet(bet: Bet, fixture: dict[str, Any]) -> BetOutcome:
|
|
||||||
goals = fixture.get("goals", {})
|
|
||||||
home = goals.get("home")
|
|
||||||
away = goals.get("away")
|
|
||||||
|
|
||||||
if home is None or away is None:
|
|
||||||
return BetOutcome.UNKNOWN
|
|
||||||
|
|
||||||
if isinstance(bet, WinDrawLose):
|
|
||||||
bet_draw = bet.betType in ("X", "0")
|
|
||||||
if bet_draw:
|
|
||||||
return BetOutcome.WIN if home == away else BetOutcome.LOSE
|
|
||||||
actual = "1" if home > away else ("0" if home == away else "2")
|
|
||||||
return BetOutcome.WIN if actual == bet.betType else BetOutcome.LOSE
|
|
||||||
|
|
||||||
if isinstance(bet, WinDrawLoseDouble):
|
|
||||||
actual = "1" if home > away else ("0" if home == away else "2")
|
|
||||||
return BetOutcome.WIN if actual in bet.betType else BetOutcome.LOSE
|
|
||||||
|
|
||||||
if isinstance(bet, WinLose):
|
|
||||||
if home == away:
|
|
||||||
return BetOutcome.VOID
|
|
||||||
actual = "1" if home > away else "2"
|
|
||||||
return BetOutcome.WIN if actual == bet.betType else BetOutcome.LOSE
|
|
||||||
|
|
||||||
if isinstance(bet, BothTeamScored):
|
|
||||||
return BetOutcome.WIN if home > 0 and away > 0 else BetOutcome.LOSE
|
|
||||||
|
|
||||||
if isinstance(bet, GoalAmount):
|
|
||||||
total = home + away
|
|
||||||
if total == bet.line:
|
|
||||||
return BetOutcome.VOID
|
|
||||||
won = total > bet.line if bet.over else total < bet.line
|
|
||||||
return BetOutcome.WIN if won else BetOutcome.LOSE
|
|
||||||
|
|
||||||
if isinstance(bet, GoalHandicap):
|
|
||||||
h_home = home + (bet.handicap_amount if bet.team_bet == "1" else 0.0)
|
|
||||||
h_away = away + (bet.handicap_amount if bet.team_bet == "2" else 0.0)
|
|
||||||
if h_home == h_away:
|
|
||||||
return BetOutcome.VOID
|
|
||||||
actual_winner = "1" if h_home > h_away else "2"
|
|
||||||
return BetOutcome.WIN if actual_winner == bet.team_bet else BetOutcome.LOSE
|
|
||||||
|
|
||||||
return BetOutcome.UNKNOWN
|
|
||||||
|
|||||||
Reference in New Issue
Block a user