Files
2026FIFAWorldCup/platform/web/lib/analytics-api.ts
QuantBot aa7e3bba76
Some checks failed
2026 World Cup Quant Platform - Production Deployment / Code Quality & Testing (push) Failing after 1m49s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Rsync (push) Has been skipped
chore: migrate deployment to Gitea Actions with zero-trust rsync
2026-06-16 19:06:50 +08:00

744 lines
19 KiB
TypeScript
Raw Permalink 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.
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<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;
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<string, number>;
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<string, number>;
};
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<string, unknown>;
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<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,
...(method === 'GET' ? { cache: 'no-store' as RequestCache } : {}),
});
if (response.ok) {
return response.json() as Promise<T>;
}
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<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 getDailyCardCalendar(startDate = '2026-06-11', endDate?: string): Promise<DailyCardCalendarResponse> {
const params = new URLSearchParams({ start_date: startDate });
if (endDate) params.set('end_date', endDate);
return requestAnalytics<DailyCardCalendarResponse>(`daily-card-calendar?${params.toString()}`, {}, 'GET');
}
export function getRecommendationPerformance(daysBack = 7): Promise<RecommendationPerformanceResponse> {
return requestAnalytics<RecommendationPerformanceResponse>(`recommendation-performance?days_back=${daysBack}`, {}, 'GET');
}
export function getAgentVerification(): Promise<AgentVerificationResponse> {
return requestAnalytics<AgentVerificationResponse>('agent-verification', {}, 'GET');
}
export function getGeminiUsage(): Promise<GeminiUsageResponse> {
return requestAnalytics<GeminiUsageResponse>('gemini-usage', {}, 'GET');
}
export function getSourceHealth(): Promise<SourceHealthResponse> {
return requestAnalytics<SourceHealthResponse>('source-health', {}, 'GET');
}
export function getAllMatches(): Promise<MatchListItem[]> {
return requestAnalytics<MatchListItem[]>('matches', {}, 'GET');
}
export function getMatchById(matchId: string): Promise<MatchDetail> {
return requestAnalytics<MatchDetail>(`matches/${matchId}`, {}, 'GET');
}