export type PlayerPropsRequestPayload = { player_id: string; player_name?: string | null; metric: 'shots' | 'shots_on_target' | 'passes'; baseline_mean: number; line: number; match_minutes: number; team_attack_factor: number; opponent_defence_factor: number; weather_fatigue_factor: number; bookmaker_over_odds?: number | null; simulations?: number; }; export type PlayerPropsResponse = { metric: string; line: number; over_probability: number; under_probability: number; expected_count: number; p5: number; p50: number; p95: number; simulation_runs: number; edge: number | null; top_edge: boolean; bookmaker_over_odds: number | null; implied_prob: number | null; }; export type KellyRequestPayload = { odds: number; true_prob: number; bankroll: number; fractional_kelly_factor: number; risk_tolerance_factor: number; }; export type KellyResponse = { odds: number; true_prob: number; bankroll: number; raw_kelly_fraction: number; fractional_kelly_factor: number; risk_tolerance_factor: number; recommended_fraction: number; recommended_stake: number; }; export type BacktestTrade = { trade_id: string; settled_at: string; odds: number; is_win: boolean; stake: number; altitude_meters: number | null; handicap: number | null; weather: string | null; recent_form_win_rate: number | null; market_type: string; selection: string; }; export type BacktestRequestPayload = { initial_capital: number; strategy: { weather: string | null; altitude_min_meters: number | null; altitude_max_meters: number | null; handicap_min: number | null; handicap_max: number | null; recent_win_rate_min: number | null; recent_win_rate_max: number | null; market_types: string[] | null; start_at: string | null; end_at: string | null; }; historical_trades: BacktestTrade[]; }; export type BacktestPoint = { ts: string; capital: number; }; export type BacktestResponse = { matched: number; total: number; hit_count: number; win_rate: number; final_capital: number; net_profit: number; roi_percent: number; max_drawdown_percent: number; equity_curve: BacktestPoint[]; }; export type MlTrainingRow = { home_rest_days: number; away_rest_days: number; home_travel_distance_km: number; away_travel_distance_km: number; recent_5_xg_home: number; recent_5_xg_away: number; match_result: 'home' | 'draw' | 'away'; }; export type MlTrainRequestPayload = { model_id?: string | null; rows: MlTrainingRow[]; }; export type MlModelEdge = { model_prob: number; implied_prob: number; edge: number; strong_buy: boolean; }; export type MlEdgeResponse = { match_id: string; model_id: string; is_fallback_model: boolean; model_probs: { home: number; draw: number; away: number; }; edges: { home: MlModelEdge; draw: MlModelEdge; away: MlModelEdge; }; strong_buy: boolean; strongest_outcome: 'home' | 'draw' | 'away'; strongest_edge_percent: number; feature_columns: string[]; training_size: number; }; export type MlTrainResponse = { model_id: string; status: string; training_size: number; is_fallback: boolean; accuracy: number | null; }; export type MlEdgeRequestPayload = { model_id?: string; match_id: string; home_rest_days: number; away_rest_days: number; home_travel_distance_km: number; away_travel_distance_km: number; recent_5_xg_home: number; recent_5_xg_away: number; home_implied_odds: number; draw_implied_odds: number; away_implied_odds: number; }; export type MatchConditionRequestPayload = { match_id: string; avg_yellow_cards: number; penalties_per_game: number; cards_ou_line: number; temp_c: number; humidity_pct: number; venue_altitude_meters: number; home_second_half_attack: number; away_second_half_attack: number; }; export type MatchConditionResponse = { match_id: string; strictness_index: number; heat_index: number; cards_pressure_alert: boolean; second_half_home_attack: number; second_half_away_attack: number; second_half_under_recommendation: boolean; attacker_direction: string; }; export type RlmRequestPayload = { match_id: string; market_type: string; selection: string; ticket_threshold?: number; odds_change_threshold?: number; }; export type RlmAlert = { match_id: string; market_type: string; selection: string; opening_odds: number; current_odds: number; ticket_pct: number; handle_pct: number; odds_change_pct: number; smart_money_to: string; is_triggered: boolean; confidence_score?: number; risk_level?: 'core' | 'speculative' | 'parlay' | 'sgp' | string; market_implied_prob?: number; edge_percent?: number; data_checks?: string[]; }; export type RlmResponse = { alerts: RlmAlert[]; total: number; }; export type ProofOfYieldSettleItem = { recommendation_id?: string | null; match_id: string; market_type: string; selection: string; stake: number; recommended_odds: number; closing_odds: number; is_win: boolean; settled_at?: string | null; }; export type ProofOfYieldSettleRequestPayload = { items: ProofOfYieldSettleItem[]; }; export type ProofOfYieldRecord = { recommendation_id: string; match_id: string; market_type: string; selection: string; stake: number; recommended_odds: number; closing_odds: number; is_win: boolean; settled_at: string; clv_percent: number | null; pnl: number; }; export type ProofOfYieldSummary = { total_recommendations: number; hit_count: number; win_rate_percent: number; total_stake: number; total_pnl: number; roi_percent: number; avg_clv_percent: number; }; export type ProofOfYieldLedgerResponse = { summary: ProofOfYieldSummary; records: ProofOfYieldRecord[]; }; export type UserBetPayload = { market_type: string; parlay_type?: string | null; odds?: number | null; stake: number; recommended_odds?: number | null; closing_odds?: number | null; is_settled: boolean; is_win: boolean; match_stage?: string | null; stage?: string | null; }; export type PortfolioLeaksRequestPayload = { user_bets: UserBetPayload[]; }; export type PortfolioLeakCluster = { market_type: string; bet_type: string; odds_bucket: string; match_stage: string; bet_count: number; total_stake: number; closed_count: number; win_count: number; total_pnl: number; avg_clv_percent: number; roi_percent: number; hit_rate_percent: number; status: string; }; export type PortfolioHardTruth = { title: string; message: string; cluster: Record; }; export type PortfolioLeaksResponse = { total_bet_count: number; settled_bet_count: number; total_stake: number; total_pnl: number; overall_roi_percent: number; overall_hit_rate_percent: number; clusters: PortfolioLeakCluster[]; hard_truths: PortfolioHardTruth[]; }; export type HedgeRequestPayload = { original_stake: number; parlay_total_odds: number; final_leg_hedge_odds: number; }; export type HedgeResponse = { hedge_stake: number; locked_profit: number; parlay_net_after_hedge_if_win: number; hedge_net_if_win: number; }; export type DailyCardLeg = { match_id: string; selection: string; odds: number; }; export type DailyCardItem = { match_id: string; match_label: string; market_type: string; selection: string; target_odds: number; win_prob: number; ev_percent: number; stake_units: number; stake_amount_twd?: number; unit_size_twd?: number; recommendation: string; rationale: string; confidence_score?: number; confidence_band?: string; confidence_factors?: string[]; data_quality?: string; has_market_odds?: boolean; odds_source_label?: string; odds_source_kind?: string; risk_level?: 'core' | 'speculative' | 'parlay' | 'sgp' | string; market_implied_prob?: number; edge_percent?: number; data_checks?: string[]; legs?: DailyCardLeg[]; }; export type DailyCardResponse = { date: string; generated_at?: string; total_daily_unit_recommendation: number; total_daily_amount_twd?: number; unit_size_twd?: number; summary: string; market_data_status?: string | null; data_quality_summary?: Record; execution_policy?: string | null; auto_refresh_seconds?: number; safe_singles: DailyCardItem[]; high_risk_singles: DailyCardItem[]; safe_parlays: DailyCardItem[]; sgp_lotteries: DailyCardItem[]; matched_matches: number; stage_distribution: Record; }; export type DailyCardCalendarDate = { date: string; match_count: number; matched_matches: number; recommendation_count: number; live_count: number; watch_count: number; safe_single_count: number; high_risk_single_count: number; safe_parlay_count: number; sgp_lottery_count: number; total_amount_twd: number; market_data_status?: string | null; snapshot_status?: string | null; snapshot_item_count?: number | null; snapshot_preserved_count?: number | null; summary?: string | null; }; export type DailyCardCalendarResponse = { generated_at: string; start_date: string; end_date: string; dates: DailyCardCalendarDate[]; }; export type RecommendationPerformanceItem = { match_id: string; match_label: string; market_type: string; selection: string; recommendation: string; result_score: string; outcome: 'hit' | 'miss' | 'push' | 'not_evaluable' | string; outcome_label: string; target_odds: number; win_prob: number; ev_percent: number; stake_units: number; stake_amount_twd?: number | null; confidence_score?: number | null; confidence_band?: string | null; has_market_odds?: boolean | null; odds_source_label?: string | null; odds_source_kind?: string | null; lesson: string; }; export type RecommendationPerformanceBucket = { market_type: string; recommendation_count: number; settled_count: number; hit_count: number; miss_count: number; push_count: number; hit_rate_percent: number; }; export type RecommendationPerformanceSourceBucket = { source_label: string; source_kind: string; recommendation_count: number; settled_count: number; hit_count: number; miss_count: number; push_count: number; hit_rate_percent: number; }; export type RecommendationPerformanceResponse = { generated_at: string; days_back: number; finished_match_count: number; rebuilt_recommendation_count: number; settled_recommendation_count: number; hit_count: number; miss_count: number; push_count: number; hit_rate_percent: number; summary: string; methodology_note: string; improvement_actions: string[]; by_market_type: RecommendationPerformanceBucket[]; by_odds_source: RecommendationPerformanceSourceBucket[]; items: RecommendationPerformanceItem[]; }; export type AgentVerificationCheck = { agent: string; role: string; status: string; status_label: string; evidence: string[]; next_action?: string | null; last_checked_at: string; }; export type AgentVerificationResponse = { generated_at: string; overall_status: string; overall_label: string; production_ready: boolean; decision_policy: string; calibration_summary: Record; checks: AgentVerificationCheck[]; }; export type GeminiUsageResponse = { generated_at: string; month: string; status: string; status_label: string; paused: boolean; cap_usd: number; estimated_cost_usd: number; remaining_usd: number; request_count: number; input_tokens: number; output_tokens: number; grounded_query_count: number; pricing_note: string; next_action?: string | null; }; export type SourceHealthResponse = { status: string; odds_coverage_status?: string; upcoming_odds_matches?: number; stale_unsettled_matches?: number; stale_unsettled_threshold_hours?: number; odds_rows: number; matches: number; finished_matches: number; venues: number; high_altitude_venues: number; latest_odds_recorded_at: string | null; latest_result_synced_at: string | null; ingestion_status?: { status?: string; source?: string; run_at?: string; events?: number; odds_rows?: number; bookmakers?: number; message?: string; } | null; fixtures_status?: { status?: string; source?: string; run_at?: string; fixtures?: number; upserted?: number; skipped?: number; interval_seconds?: number; message?: string; } | null; news_status?: { status?: string; source?: string; run_at?: string; items?: number; interval_seconds?: number; message?: string; } | null; provider_requirements?: { primary_odds_provider?: string; required_markets?: string[]; taiwan_sports_lottery?: string; current_limitation?: string; }; }; export type MatchListItem = { match_id: string; home_team: string; away_team: string; home_score: number | null; away_score: number | null; kickoff_utc: string; status: string; venue_name: string | null; venue_city: string | null; venue_country: string | null; }; export type MatchOddsPoint = { recorded_at: string; bookmaker: string; bookmaker_id: string; market_type: string; selection: string; decimal_odds: number; implied_probability: number; }; export type MatchPoisson = { expected_home_goals: number; expected_away_goals: number; score_matrix: number[][]; one_x_two: { home_win: number; draw: number; away_win: number; }; over_under_2_5: { under: number; over: number; }; }; export type MatchConditionsReadout = { strictness_index: number; heat_index: number; cards_pressure_alert: boolean; second_half_home_attack: number; second_half_away_attack: number; second_half_under_recommendation: boolean; attacker_direction: string; }; export type MatchDetail = { match_id: string; home_team: string; away_team: string; home_score: number | null; away_score: number | null; home_xg: number; away_xg: number; match_time_utc: string; status: string; venue_name: string; venue_city: string | null; venue_country: string | null; venue_altitude_meters: number | null; odds_series: MatchOddsPoint[]; odds_quality?: string; xg_quality?: string; poisson: MatchPoisson; conditions: MatchConditionsReadout; quant_summary: string; }; type ApiErrorPayload = { message?: string; detail?: string; error?: { message?: string; detail?: string; }; }; const ANALYTICS_API_BASE = '/api/analytics'; type HttpMethod = 'GET' | 'POST'; async function requestAnalytics( path: string, payload: unknown, method: HttpMethod = 'POST', ): Promise { try { const response = await fetch(`${ANALYTICS_API_BASE}/${path}`, { method, headers: { 'Content-Type': 'application/json' }, body: method === 'POST' ? JSON.stringify(payload) : undefined, ...(method === 'GET' ? { cache: 'no-store' as RequestCache } : {}), }); if (response.ok) { return response.json() as Promise; } const contentType = response.headers.get('content-type') || ''; const errorPayload = contentType.includes('application/json') ? ((await response.json().catch(() => null)) as ApiErrorPayload | null) : null; const textPayload = errorPayload ? '' : await response.text().catch(() => ''); const backendMessage = errorPayload?.error?.message || errorPayload?.error?.detail || errorPayload?.message || errorPayload?.detail || textPayload || '資料服務暫時無法回應'; throw new Error(`${backendMessage}(狀態碼 ${response.status})`); } catch (error) { console.error(`[API Error] Fetch failed for ${path}`, error); throw error; } } export function calculatePlayerProps(payload: PlayerPropsRequestPayload): Promise { return requestAnalytics('player-props', payload); } export function calculateKelly(payload: KellyRequestPayload): Promise { return requestAnalytics('kelly', payload); } export function runBacktest(payload: BacktestRequestPayload): Promise { return requestAnalytics('backtest', payload); } export function trainMlModel(payload: MlTrainRequestPayload): Promise { return requestAnalytics('ml-edge/train', payload); } export function analyzeMlEdge(payload: MlEdgeRequestPayload): Promise { return requestAnalytics('ml-edge', payload); } export function analyzeMatchConditions(payload: MatchConditionRequestPayload): Promise { return requestAnalytics('match-conditions', payload); } export function detectReverseLineMovement(payload: RlmRequestPayload): Promise { return requestAnalytics('rlm', payload); } export function getProofOfYieldLedger(limit = 200): Promise { return requestAnalytics(`proof-of-yield/ledger?limit=${limit}`, {}, 'GET'); } export function settleProofOfYield(payload: ProofOfYieldSettleRequestPayload): Promise { return requestAnalytics('proof-of-yield/settle', payload); } export function analyzePortfolioLeaks(payload: PortfolioLeaksRequestPayload): Promise { return requestAnalytics('portfolio/leaks', payload); } export function calculateHedge(payload: HedgeRequestPayload): Promise { return requestAnalytics('hedging', payload); } export function getDailyCard(date: string): Promise { return requestAnalytics(`daily-card/${date}`, {}, 'GET'); } export function getDailyCardCalendar(startDate = '2026-06-11', endDate?: string): Promise { const params = new URLSearchParams({ start_date: startDate }); if (endDate) params.set('end_date', endDate); return requestAnalytics(`daily-card-calendar?${params.toString()}`, {}, 'GET'); } export function getRecommendationPerformance(daysBack = 7): Promise { return requestAnalytics(`recommendation-performance?days_back=${daysBack}`, {}, 'GET'); } export function getAgentVerification(): Promise { return requestAnalytics('agent-verification', {}, 'GET'); } export function getGeminiUsage(): Promise { return requestAnalytics('gemini-usage', {}, 'GET'); } export function getSourceHealth(): Promise { return requestAnalytics('source-health', {}, 'GET'); } export function getAllMatches(): Promise { return requestAnalytics('matches', {}, 'GET'); } export function getMatchById(matchId: string): Promise { return requestAnalytics(`matches/${matchId}`, {}, 'GET'); }