Initial commit with 2026 World Cup Quant Platform core modules and CI/CD
This commit is contained in:
508
platform/web/lib/analytics-api.ts
Normal file
508
platform/web/lib/analytics-api.ts
Normal file
@@ -0,0 +1,508 @@
|
||||
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> {
|
||||
const response = await fetch(`${ANALYTICS_API_BASE}/${path}`, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: method === 'POST' ? JSON.stringify(payload) : undefined,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let message = `Analytics API 錯誤:${response.status}`;
|
||||
try {
|
||||
const body = (await response.json()) as ApiErrorPayload;
|
||||
if (body?.message) {
|
||||
message = body.message;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
message = error.message;
|
||||
}
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return response.json() as Promise<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');
|
||||
}
|
||||
Reference in New Issue
Block a user