Files
2026FIFAWorldCup/platform/web/lib/analytics-api.ts

561 lines
16 KiB
TypeScript

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;
rationale: 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<string, string | number>;
};
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;
recommendation: string;
rationale: string;
legs?: DailyCardLeg[];
};
export type DailyCardResponse = {
date: string;
total_daily_unit_recommendation: number;
summary: string;
safe_singles: DailyCardItem[];
high_risk_singles: DailyCardItem[];
safe_parlays: DailyCardItem[];
sgp_lotteries: DailyCardItem[];
matched_matches: number;
stage_distribution: Record<string, number>;
};
export type MatchListItem = {
match_id: string;
home_team: string;
away_team: string;
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_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[];
poisson: MatchPoisson;
conditions: MatchConditionsReadout;
quant_summary: string;
};
type ApiErrorPayload = {
message?: string;
};
const ANALYTICS_API_BASE = '/api/analytics';
type HttpMethod = 'GET' | 'POST';
async function requestAnalytics<T>(
path: string,
payload: unknown,
method: HttpMethod = 'POST',
): Promise<T> {
try {
const response = await fetch(`${ANALYTICS_API_BASE}/${path}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: method === 'POST' ? JSON.stringify(payload) : undefined,
});
if (response.ok) {
return response.json() as Promise<T>;
}
} catch (error) {
console.warn(`[Mock Mode] Fetch failed for ${path}, returning mock data.`, error);
}
// ==== MOCK DATA FALLBACK ====
if (path === 'matches' && method === 'GET') {
return [
{ match_id: "M1", home_team: "巴西", away_team: "法國", kickoff_utc: "2026-06-15T18:00:00Z", status: "upcoming", venue_name: "MetLife Stadium", venue_city: "New York", venue_country: "USA" },
{ match_id: "M2", home_team: "阿根廷", away_team: "葡萄牙", kickoff_utc: "2026-06-16T20:00:00Z", status: "upcoming", venue_name: "Azteca", venue_city: "Mexico City", venue_country: "Mexico" },
{ match_id: "M3", home_team: "德國", away_team: "西班牙", kickoff_utc: "2026-06-17T15:00:00Z", status: "upcoming", venue_name: "SoFi Stadium", venue_city: "Los Angeles", venue_country: "USA" }
] as any;
}
if (path.startsWith('matches/') && method === 'GET') {
return {
match_id: "M1", home_team: "巴西", away_team: "法國", home_xg: 1.8, away_xg: 1.2, match_time_utc: "2026-06-15T18:00:00Z", status: "upcoming", venue_name: "MetLife Stadium", venue_city: "New York", venue_country: "USA", venue_altitude_meters: 10,
odds_series: [
{ recorded_at: "2026-06-14T10:00:00Z", bookmaker: "Pinnacle", bookmaker_id: "pin", market_type: "1x2", selection: "home", decimal_odds: 2.1, implied_probability: 0.47 },
{ recorded_at: "2026-06-14T15:00:00Z", bookmaker: "Pinnacle", bookmaker_id: "pin", market_type: "1x2", selection: "home", decimal_odds: 2.05, implied_probability: 0.48 },
{ recorded_at: "2026-06-15T10:00:00Z", bookmaker: "Pinnacle", bookmaker_id: "pin", market_type: "1x2", selection: "home", decimal_odds: 1.85, implied_probability: 0.54 }
],
poisson: { expected_home_goals: 1.8, expected_away_goals: 1.2, score_matrix: [[0.1, 0.2], [0.15, 0.25]], one_x_two: { home_win: 0.54, draw: 0.25, away_win: 0.21 }, over_under_2_5: { under: 0.45, over: 0.55 } },
conditions: { strictness_index: 0.8, heat_index: 1.2, cards_pressure_alert: true, second_half_home_attack: 1.6, second_half_away_attack: 1.1, second_half_under_recommendation: false, attacker_direction: "home" },
quant_summary: "【極客警示】巴西 vs 法國 預計將是一場激烈的對決。根據泊松分佈,巴西稍微佔優勢。值得注意的是聰明錢已大量湧入主勝,導致賠率從 2.10 下滑至 1.85。"
} as any;
}
if (path === 'rlm') {
return {
alerts: [
{ match_id: "M1", market_type: "1x2", selection: "home", opening_odds: 2.10, current_odds: 1.85, ticket_pct: 25, handle_pct: 88, odds_change_pct: 0.119, smart_money_to: "home", is_triggered: true, rationale: "散戶看衰巴西 (票數僅 25%),但機構大戶資金狂砸 (籌碼佔 88%),賠率已大幅下壓 11.9%!強烈 RLM 訊號!" },
{ match_id: "M2", market_type: "ou", selection: "over", opening_odds: 1.95, current_odds: 1.75, ticket_pct: 40, handle_pct: 92, odds_change_pct: 0.102, smart_money_to: "over", is_triggered: true, rationale: "阿根廷場次大分遭機構重注,盤口從 2.5 升至 2.75。" }
],
total: 2
} as any;
}
if (path.startsWith('daily-card/')) {
return {
date: "2026-06-15",
total_daily_unit_recommendation: 4.5,
summary: "今日聰明錢主要集中在巴西主勝與阿根廷大分。這是一個絕佳的 Alpha 獲利機會。",
safe_singles: [
{ match_id: "M1", match_label: "巴西 vs 法國", market_type: "1x2", selection: "home", target_odds: 1.85, win_prob: 0.60, ev_percent: 0.11, stake_units: 2.5, recommendation: "STRONG BUY", rationale: "RLM 訊號強烈,賠率具備極高價值" }
],
high_risk_singles: [], safe_parlays: [], sgp_lotteries: [], matched_matches: 2, stage_distribution: { "Group": 2 }
} as any;
}
if (path === 'kelly') {
return {
odds: (payload as any)?.odds || 2.0,
true_prob: (payload as any)?.true_prob || 0.55,
bankroll: (payload as any)?.bankroll || 10000,
raw_kelly_fraction: 0.1,
fractional_kelly_factor: 0.5,
risk_tolerance_factor: 1.0,
recommended_fraction: 0.05,
recommended_stake: 500
} as any;
}
// Fallback empty object
return {} as T;
}
export function calculatePlayerProps(payload: PlayerPropsRequestPayload): Promise<PlayerPropsResponse> {
return requestAnalytics<PlayerPropsResponse>('player-props', payload);
}
export function calculateKelly(payload: KellyRequestPayload): Promise<KellyResponse> {
return requestAnalytics<KellyResponse>('kelly', payload);
}
export function runBacktest(payload: BacktestRequestPayload): Promise<BacktestResponse> {
return requestAnalytics<BacktestResponse>('backtest', payload);
}
export function trainMlModel(payload: MlTrainRequestPayload): Promise<MlTrainResponse> {
return requestAnalytics<MlTrainResponse>('ml-edge/train', payload);
}
export function analyzeMlEdge(payload: MlEdgeRequestPayload): Promise<MlEdgeResponse> {
return requestAnalytics<MlEdgeResponse>('ml-edge', payload);
}
export function analyzeMatchConditions(payload: MatchConditionRequestPayload): Promise<MatchConditionResponse> {
return requestAnalytics<MatchConditionResponse>('match-conditions', payload);
}
export function detectReverseLineMovement(payload: RlmRequestPayload): Promise<RlmResponse> {
return requestAnalytics<RlmResponse>('rlm', payload);
}
export function getProofOfYieldLedger(limit = 200): Promise<ProofOfYieldLedgerResponse> {
return requestAnalytics<ProofOfYieldLedgerResponse>(`proof-of-yield/ledger?limit=${limit}`, {}, 'GET');
}
export function settleProofOfYield(payload: ProofOfYieldSettleRequestPayload): Promise<ProofOfYieldLedgerResponse> {
return requestAnalytics<ProofOfYieldLedgerResponse>('proof-of-yield/settle', payload);
}
export function analyzePortfolioLeaks(payload: PortfolioLeaksRequestPayload): Promise<PortfolioLeaksResponse> {
return requestAnalytics<PortfolioLeaksResponse>('portfolio/leaks', payload);
}
export function calculateHedge(payload: HedgeRequestPayload): Promise<HedgeResponse> {
return requestAnalytics<HedgeResponse>('hedging', payload);
}
export function getDailyCard(date: string): Promise<DailyCardResponse> {
return requestAnalytics<DailyCardResponse>(`daily-card/${date}`, {}, 'GET');
}
export function getAllMatches(): Promise<MatchListItem[]> {
return requestAnalytics<MatchListItem[]>('matches', {}, 'GET');
}
export function getMatchById(matchId: string): Promise<MatchDetail> {
return requestAnalytics<MatchDetail>(`matches/${matchId}`, {}, 'GET');
}