diff --git a/platform/backend/app/analytics/sgp_engine.py b/platform/backend/app/analytics/sgp_engine.py index 62b9657..5aaa628 100644 --- a/platform/backend/app/analytics/sgp_engine.py +++ b/platform/backend/app/analytics/sgp_engine.py @@ -28,11 +28,11 @@ class SGPCorrelationEngine: if correlation_coeff == 0: return independent_joint_prob elif correlation_coeff > 0: - # 正相關:聯合機率向 max_joint_prob 靠攏 - return independent_joint_prob + correlation_coeff * (max_joint_prob - independent_joint_prob) + # 正相關:使用 Fréchet–Hoeffding 上界與獨立機率的非線性加權,避免極端值的線性失真 + return independent_joint_prob + (max_joint_prob - independent_joint_prob) * (correlation_coeff ** 1.5) else: - # 負相關:聯合機率向 min_joint_prob 靠攏 - return independent_joint_prob + abs(correlation_coeff) * (min_joint_prob - independent_joint_prob) + # 負相關:使用 Fréchet–Hoeffding 下界與獨立機率的非線性加權 + return independent_joint_prob - (independent_joint_prob - min_joint_prob) * (abs(correlation_coeff) ** 1.5) @staticmethod def find_sgp_value(events: List[Dict], bookmaker_sgp_odds: float) -> Dict: diff --git a/platform/backend/app/api/daily_card_generator.py b/platform/backend/app/api/daily_card_generator.py index f0d43b1..47bc92e 100644 --- a/platform/backend/app/api/daily_card_generator.py +++ b/platform/backend/app/api/daily_card_generator.py @@ -1,5 +1,8 @@ from datetime import date from typing import List, Dict +from sqlalchemy.future import select +from sqlalchemy.orm import aliased +from .db.models import Match, OddsHistory, Team class DailyCardGenerator: """ @@ -9,13 +12,12 @@ class DailyCardGenerator: def __init__(self, db_session): self.db = db_session - def generate_daily_card(self, target_date: date) -> Dict: + async def generate_daily_card(self, target_date: date) -> Dict: """ 掃描當日賽事,並將高價值投注分類打包 """ - # 模擬從資料庫與 EV 引擎取得的當日高價值清單 - # 實務上會 join `matches` 與 `odds_history` 並即時套用 ev_calculator - raw_value_bets = self._fetch_todays_value_bets(target_date) + # 真實自資料庫取得當日高價值清單 + raw_value_bets = await self._fetch_todays_value_bets(target_date) card = { "date": target_date.isoformat(), @@ -41,9 +43,47 @@ class DailyCardGenerator: return card - def _fetch_todays_value_bets(self, target_date: date) -> List[Dict]: - # 模擬資料 - return [ - {"match": "USA vs ENG", "selection": "Under 2.5", "odds": 1.95, "true_prob": 0.58, "ev_percentage": 0.131}, - {"match": "MEX vs ARG", "selection": "MEX Win", "odds": 4.20, "true_prob": 0.28, "ev_percentage": 0.176} - ] + async def _fetch_todays_value_bets(self, target_date: date) -> List[Dict]: + home_team = aliased(Team) + away_team = aliased(Team) + + # 實作:從資料庫撈取當日賽事與最新賠率 + stmt = ( + select( + Match, + home_team.name.label("home_name"), + away_team.name.label("away_name"), + OddsHistory + ) + .join(home_team, Match.home_team_id == home_team.id) + .join(away_team, Match.away_team_id == away_team.id) + .join(OddsHistory, Match.id == OddsHistory.match_id) + .where( + # 此處僅為範例,實務需處理 timezone 與日期邊界 + Match.match_time_utc >= target_date + ) + .order_by(OddsHistory.recorded_at.desc()) + .limit(100) + ) + + result = await self.db.execute(stmt) + rows = result.all() + + bets = [] + for match, home_name, away_name, odds in rows: + # 簡化 EV 計算 + implied_prob = odds.implied_probability + # 假設 true_prob 透過量化引擎取得 (此處以微小偏移模擬) + true_prob = implied_prob * 1.05 if implied_prob > 0 else 0.0 + ev = (true_prob * float(odds.decimal_odds)) - 1.0 + + if ev > 0: + bets.append({ + "match": f"{home_name} vs {away_name}", + "selection": f"{odds.market_type} - {odds.selection}", + "odds": float(odds.decimal_odds), + "true_prob": true_prob, + "ev_percentage": ev + }) + + return bets diff --git a/platform/web/components/RLMRadarBoard.tsx b/platform/web/components/RLMRadarBoard.tsx index f20c645..164d9df 100644 --- a/platform/web/components/RLMRadarBoard.tsx +++ b/platform/web/components/RLMRadarBoard.tsx @@ -41,7 +41,7 @@ export default function RlmRadarBoard({ alerts }: RlmRadarBoardProps) { -