Files
2026FIFAWorldCup/platform/backend/app/ingestion/cache.py

137 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import Any, Dict, Mapping
from redis.asyncio import Redis
ARBITRAGE_LUA = r'''
local odds_json = ARGV[1]
local payload = cjson.decode(odds_json)
local grouped = {}
for _, row in ipairs(payload) do
local market = row.market_type
local selection = row.selection
local odds = tonumber(row.decimal_odds)
if market and selection and odds and odds > 0 then
if grouped[market] == nil then
grouped[market] = {}
end
if grouped[market][selection] == nil or odds > grouped[market][selection] then
grouped[market][selection] = odds
end
end
end
local out = {}
for market, selections in pairs(grouped) do
local prob_sum = 0
local count = 0
for _, odds in pairs(selections) do
prob_sum = prob_sum + (1 / odds)
count = count + 1
end
if count > 1 then
out[market] = {
has_arbitrage = (prob_sum < 1),
implied_total_probability = prob_sum,
edge = math.max(1 - prob_sum, 0),
best_odds = selections,
}
end
end
return cjson.encode(out)
'''
@dataclass(slots=True)
class MatchState:
"""賽中 Hash 快照欄位。"""
home_score: int
away_score: int
possession_home_pct: float
possession_away_pct: float
red_cards_home: int
red_cards_away: int
class MatchCacheManager:
"""賽事 Redis 快取層。
- live:{match_id}:odds 存 JSON即時賠率
- live:{match_id}:state 存 Hash包含比分、控球率、紅牌數
"""
def __init__(self, redis: Redis) -> None:
self.redis = redis
self._lua_sha: str | None = None
async def _ensure_lua(self) -> str:
if self._lua_sha is None:
self._lua_sha = await self.redis.script_load(ARBITRAGE_LUA)
return self._lua_sha
async def set_match_odds(
self,
match_id: str,
payload: list[dict[str, Any]],
*,
ttl_seconds: int = 45,
finished: bool = False,
) -> None:
key = f'live:{match_id}:odds'
value = json.dumps(payload, ensure_ascii=False)
ttl = 7200 if finished else ttl_seconds
await self.redis.set(name=key, value=value, ex=ttl)
async def get_match_odds(self, match_id: str) -> list[dict[str, Any]]:
key = f'live:{match_id}:odds'
raw = await self.redis.get(key)
if not raw:
return []
if isinstance(raw, bytes):
raw = raw.decode()
return json.loads(raw)
async def set_match_state(
self,
match_id: str,
state: MatchState | Mapping[str, Any],
*,
ttl_seconds: int = 7200,
) -> None:
key = f'live:{match_id}:state'
mapping = {
'home_score': state['home_score'] if isinstance(state, Mapping) else state.home_score,
'away_score': state['away_score'] if isinstance(state, Mapping) else state.away_score,
'possession_home_pct': state['possession_home_pct'] if isinstance(state, Mapping) else state.possession_home_pct,
'possession_away_pct': state['possession_away_pct'] if isinstance(state, Mapping) else state.possession_away_pct,
'red_cards_home': state['red_cards_home'] if isinstance(state, Mapping) else state.red_cards_home,
'red_cards_away': state['red_cards_away'] if isinstance(state, Mapping) else state.red_cards_away,
}
await self.redis.hset(name=key, mapping=mapping)
await self.redis.expire(key, ttl_seconds)
async def get_match_state(self, match_id: str) -> dict[str, str] | None:
key = f'live:{match_id}:state'
result = await self.redis.hgetall(key)
return {str(k): str(v) for k, v in result.items()} if result else None
async def calculate_arbitrage_for_match(self, match_id: str) -> dict[str, Any]:
odds = await self.get_match_odds(match_id)
if not odds:
return {}
sha = await self._ensure_lua()
result = await self.redis.evalsha(sha, 0, json.dumps(odds, ensure_ascii=False))
if isinstance(result, bytes):
result = result.decode()
if isinstance(result, str):
return json.loads(result)
return result