Initial commit with 2026 World Cup Quant Platform core modules and CI/CD
This commit is contained in:
134
platform/backend/app/services/redis_manager.py
Normal file
134
platform/backend/app/services/redis_manager.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Redis 快取管理層(賠率與賽事快取)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Mapping
|
||||
|
||||
from redis.asyncio import Redis
|
||||
|
||||
|
||||
ARBITRAGE_LUA = r'''
|
||||
local odds_json = ARGV[1]
|
||||
local payload = cjson.decode(odds_json)
|
||||
|
||||
local by_market = {}
|
||||
for _, row in ipairs(payload) do
|
||||
local market = row.market_type
|
||||
local selection = row.selection
|
||||
local odds = tonumber(row.decimal_odds)
|
||||
local bookmaker = tostring(row.bookmaker or "")
|
||||
|
||||
if market and selection and odds and odds > 0 then
|
||||
if by_market[market] == nil then
|
||||
by_market[market] = {}
|
||||
end
|
||||
if by_market[market][selection] == nil or odds > by_market[market][selection].odds then
|
||||
by_market[market][selection] = {odds = odds, bookmaker = bookmaker}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local out = {}
|
||||
for market, selections in pairs(by_market) do
|
||||
local inv = 0
|
||||
local n = 0
|
||||
for _, item in pairs(selections) do
|
||||
inv = inv + (1 / item.odds)
|
||||
n = n + 1
|
||||
end
|
||||
if n >= 2 then
|
||||
out[market] = {
|
||||
has_arbitrage = (inv < 1),
|
||||
implied_total = inv,
|
||||
best_odds = selections,
|
||||
edge = math.max(1 - inv, 0)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return cjson.encode(out)
|
||||
'''
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MatchState:
|
||||
home_score: int
|
||||
away_score: int
|
||||
minute: int
|
||||
possession_home: float
|
||||
possession_away: float
|
||||
red_cards_home: int
|
||||
red_cards_away: int
|
||||
|
||||
|
||||
class MatchCacheManager:
|
||||
"""實作高頻快取:賠率 JSON + 賽事狀態 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]],
|
||||
*,
|
||||
finished: bool = False,
|
||||
) -> None:
|
||||
key = f'live_odds:{match_id}'
|
||||
ex = 7200 if finished else 30
|
||||
await self.redis.set(key, json.dumps(payload, ensure_ascii=False), ex=ex)
|
||||
|
||||
async def get_match_odds(self, match_id: str) -> list[dict[str, Any]]:
|
||||
key = f'live_odds:{match_id}'
|
||||
raw = await self.redis.get(key)
|
||||
if not raw:
|
||||
return []
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode('utf-8')
|
||||
return json.loads(raw)
|
||||
|
||||
async def set_match_state(
|
||||
self,
|
||||
match_id: str,
|
||||
state: MatchState | Mapping[str, Any],
|
||||
*,
|
||||
finished: bool = False,
|
||||
) -> None:
|
||||
key = f'live_state:{match_id}'
|
||||
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,
|
||||
'minute': state['minute'] if isinstance(state, Mapping) else state.minute,
|
||||
'possession_home': state['possession_home'] if isinstance(state, Mapping) else state.possession_home,
|
||||
'possession_away': state['possession_away'] if isinstance(state, Mapping) else state.possession_away,
|
||||
'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(key, mapping=mapping)
|
||||
await self.redis.expire(key, 7200 if finished else 60)
|
||||
|
||||
async def get_match_state(self, match_id: str) -> dict[str, str] | None:
|
||||
key = f'live_state:{match_id}'
|
||||
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(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
|
||||
Reference in New Issue
Block a user