UNPKG

metafide-surge

Version:

Metafide Surge Game Computation

513 lines (463 loc) 15.7 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ import { SpotGame, SpotPlayer, RangeGame, RangePlayer, PotDistribution, AllocationPercentages, SpotPlayerVarianceResult, RangePlayerVarianceResult, ComputedData, } from './types'; import { DEFAULT_RANGE_ALLOCATION_PERCENTAGES, DEFAULT_SPOT_ALLOCATION_PERCENTAGES, serializeRangePlayer, serializeSpotPlayer, parseNumeric, } from './utils'; // RANGE CALCULATION /** * Calculates the pot distribution for a game based on players' contributions * and given allocation percentages. * * @param players - List of players participating in the game * @param allocations - Optional custom allocation percentages (defaults to predefined values) * @returns An object containing the breakdown of the total pot into different allocations */ function calculateRangePotDistribution( data: RangePlayer[], allocations: AllocationPercentages = DEFAULT_RANGE_ALLOCATION_PERCENTAGES ): PotDistribution { // If there are no players, return an empty result immediately if (data.length === 0) { return { total_pot: 0, metafide_rake: 0, streak_pot_5: 0, streak_pot_10: 0, streak_pot_25: 0, daily_reward_pot: 0, monthly_reward_pot: 0, burn: 0, winning_pot: 0, }; } const players = data.map(serializeRangePlayer); const total_pot = players.reduce( (sum, player) => sum + parseNumeric(player.f), 0 ); // Compute all allocations using the provided percentages const metafide_rake = parseFloat( (total_pot * allocations.metafide_rake).toFixed(2) ); const streak_pot_5 = parseFloat( (total_pot * allocations.streak_pot_5).toFixed(2) ); const streak_pot_10 = parseFloat( (total_pot * allocations.streak_pot_10).toFixed(2) ); const streak_pot_25 = parseFloat( (total_pot * allocations.streak_pot_25).toFixed(2) ); const daily_reward_pot = parseFloat( (total_pot * allocations.daily_reward_pot).toFixed(2) ); const monthly_reward_pot = parseFloat( (total_pot * allocations.monthly_reward_pot).toFixed(2) ); const burn = parseFloat((total_pot * allocations.burn).toFixed(2)); // Sum all deductions const totalDeductions = metafide_rake + streak_pot_5 + streak_pot_10 + streak_pot_25 + daily_reward_pot + monthly_reward_pot + burn; // Compute the winning pot (remaining after deductions) const winning_pot = parseFloat((total_pot - totalDeductions).toFixed(2)); // Return a structured breakdown return { total_pot: parseFloat(total_pot.toFixed(2)), metafide_rake, streak_pot_5, streak_pot_10, streak_pot_25, daily_reward_pot, monthly_reward_pot, burn, winning_pot, }; } /** * Computes the variance between players' predicted price ranges and the actual game price range. * * @param players - List of players participating in the game * @param game - The game data including actual price boundaries and timestamps * @returns An array of PlayerVarianceResult containing each player's variance data and total variance sum */ function computeRangePlayerVariance( players: RangePlayer[], game: RangeGame ): RangePlayerVarianceResult[] { // If there are no players, return an empty result immediately if (players.length === 0) return []; // Prepare a simplified game data object for easy access const rangeGameData: Record<string, number> = { end_timestamp: Math.floor(new Date(game.t3).getTime() / 1000), high_actual: game.ha as number, // high actual price (guaranteed non-null) low_actual: game.la as number, // low actual price (guaranteed non-null) gid: game.gid, // game ID }; // Calculate each player's variance based on their predictions vs actual prices const varianceData = players.map(p => { let total_variance = 0; // Only compute variance if the player's predicted high is above actual high // AND predicted low is below actual low (player's range fully covers the actual range) if ( parseNumeric(p.hp) > rangeGameData.high_actual && parseNumeric(p.lp) < rangeGameData.low_actual ) { total_variance = Math.pow( Math.max(parseNumeric(p.hp) - rangeGameData.high_actual, 0) + Math.max(rangeGameData.low_actual - parseNumeric(p.lp), 0), 1.5 ); } // Return the basic variance data for this player return { ...p, varianceInput: { high_actual: rangeGameData.high_actual, low_actual: rangeGameData.low_actual, total_variance, // computed variance }, }; }); // Compute the total variance across all players const sumTotalVariance = varianceData.reduce( (sum, v) => sum + v.varianceInput.total_variance, 0 ); // Merge the total variance into each player's result and return the final array return varianceData.map(v => ({ ...v, varianceInput: { high_actual: v.varianceInput.high_actual, low_actual: v.varianceInput.low_actual, total_variance: v.varianceInput.total_variance, sum_total_variance: parseFloat(sumTotalVariance.toFixed(6)), }, })); } /** * Handles the full computation of player winnings based on their variance and the game's pot distribution. * * @param players - List of players participating in the game * @param games - The game data including actual price boundaries and timestamps * @param distribution - The Range Game Distribution * @returns An object containing detailed winnings per player, highest winnings, and largest returns */ function computeRangeWinnings( data: RangePlayer[], games: RangeGame, distribution: PotDistribution ): ComputedData { // If there are no players, immediately return empty results if (data.length === 0) { return { players: [], streak: { winnings: [], returns: [], }, }; } // Step 1: Compute total pot distribution and player variances const players = data.map(serializeRangePlayer); const computedPlayers = computeRangePlayerVariance(players, games); // Step 2: Calculate the total sum of variances const sumTotalVariance = computedPlayers.reduce( (sum, player) => sum + player.varianceInput.total_variance, 0 ); // Step 4: Handle the special case where no player has any variance if (sumTotalVariance === 0) { return { players: computedPlayers.map(player => ({ ...player, w: 0, r: 0, })), streak: { winnings: [], returns: [], }, }; } // Step 5: Precompute the sum of (shareTokensToVariance) for normalization const sumShareTokensToVariance = computedPlayers.reduce((sum, player) => { if (player.varianceInput.total_variance === 0) return sum; // Ignore players with zero variance const shareVariance = player.varianceInput.total_variance / sumTotalVariance; const shareTokens = parseNumeric(player.f) / distribution.total_pot; const shareTokensToVariance = shareTokens / shareVariance; return sum + shareTokensToVariance; }, 0); // Step 6: Compute each player's normalized winnings and percent return const updatedPlayers = computedPlayers.map(player => { if (player.varianceInput.total_variance === 0) { return { ...player, w: 0, r: 0 }; } const shareVariance = player.varianceInput.total_variance / sumTotalVariance; const shareTokens = parseNumeric(player.f) / distribution.total_pot; const shareTokensToVariance = shareTokens / shareVariance; const normalizedTokensToVariance = shareTokensToVariance / sumShareTokensToVariance; // Raw winnings in system's units, then converted down const winningsRaw = normalizedTokensToVariance * distribution.winning_pot; // Percent return relative to player's initial stake const percentReturn = ((winningsRaw - parseNumeric(player.f)) / parseNumeric(player.f)) * 100; const { varianceInput, ...rest } = player; return { ...rest, w: winningsRaw, r: percentReturn }; }); // Step 7: Determine the number of top players (2% of total players, rounded up) const totalPlayers = updatedPlayers.length; const topPlayersCount = Math.ceil(totalPlayers * 0.02); // Step 8: Find top players by highest winnings const highestWinnings = [...updatedPlayers] .sort((a, b) => b.w - a.w) // Sort descending by winnings .slice(0, topPlayersCount); // Step 9: Find top players by highest percent return const largestReturns = [...updatedPlayers] .sort((a, b) => b.r - a.r) // Sort descending by percent return .slice(0, topPlayersCount); // Step 10: Return the full computation result return { players: updatedPlayers, streak: { winnings: highestWinnings, returns: largestReturns, }, }; } // SPOT COMPUTATION function calculateSpotPotDistribution( data: SpotPlayer[], allocations: AllocationPercentages = DEFAULT_SPOT_ALLOCATION_PERCENTAGES ): PotDistribution { if (data.length === 0) { return { total_pot: 0, metafide_rake: 0, range_rake: 0, streak_pot_5: 0, streak_pot_10: 0, streak_pot_25: 0, daily_reward_pot: 0, monthly_reward_pot: 0, burn: 0, winning_pot: 0, }; } const players = data.map(serializeSpotPlayer); const total_pot = players.reduce( (sum, player) => sum + parseNumeric(player.f), 0 ); const metafide_rake = parseFloat( (total_pot * allocations.metafide_rake).toFixed(2) ); const streak_pot_5 = parseFloat( (total_pot * allocations.streak_pot_5).toFixed(2) ); const streak_pot_10 = parseFloat( (total_pot * allocations.streak_pot_10).toFixed(2) ); const streak_pot_25 = parseFloat( (total_pot * allocations.streak_pot_25).toFixed(2) ); const daily_reward_pot = parseFloat( (total_pot * allocations.daily_reward_pot).toFixed(2) ); const monthly_reward_pot = parseFloat( (total_pot * allocations.monthly_reward_pot).toFixed(2) ); const burn = parseFloat((total_pot * allocations.burn).toFixed(2)); const range_rake = allocations.range_rake ? parseFloat((total_pot * allocations.range_rake).toFixed(2)) : 0; // Sum all deductions including optional range rake const totalDeductions = metafide_rake + streak_pot_5 + streak_pot_10 + streak_pot_25 + daily_reward_pot + monthly_reward_pot + burn + range_rake; const winning_pot = parseFloat((total_pot - totalDeductions).toFixed(2)); return { total_pot: parseFloat(total_pot.toFixed(2)), metafide_rake, streak_pot_5, streak_pot_10, streak_pot_25, daily_reward_pot, monthly_reward_pot, burn, winning_pot, range_rake, }; } /** * Computes the spot variance between players' predicted price ranges and the actual game price range. * * @param players - List of players participating in the game * @param game - The game data including actual price boundaries and timestamps * @returns An array of PlayerVarianceResult containing each player's variance data and total variance sum */ function computeSpotPlayerVariance( players: SpotPlayer[], game: SpotGame ): SpotPlayerVarianceResult[] { if (players.length === 0) return []; const closedTimestamp = Number(game.t3); // Already in seconds const actualPrice = Number(game.ca); const spotVariance = players.map(player => { const spotTimestamp = Math.floor(Number(player.t) / 1000); const timestampDiff = closedTimestamp - spotTimestamp; const absSpotVar = Math.abs(actualPrice - Number(player.sp)); const tokenMilliseconds = Math.pow(51250 - timestampDiff, 1.25) * Number(player.f); return { ...player, varianceInput: { actual_price: actualPrice, abs_spot_var: absSpotVar, token_milliseconds: tokenMilliseconds, }, }; }); const sumAbsSpotVar = spotVariance.reduce( (sum, p) => sum + p.varianceInput.abs_spot_var, 0 ); const sumTokenMilliseconds = spotVariance.reduce( (sum, p) => sum + p.varianceInput.token_milliseconds, 0 ); const shareVarianceData = spotVariance.map(p => { const shareTkmToVar = p.varianceInput.abs_spot_var === 0 || sumAbsSpotVar === 0 ? 0 : p.varianceInput.token_milliseconds / sumTokenMilliseconds / (p.varianceInput.abs_spot_var / sumAbsSpotVar); return { ...p, varianceInput: { actual_price: p.varianceInput.actual_price, abs_spot_var: p.varianceInput.abs_spot_var, token_milliseconds: p.varianceInput.token_milliseconds, share_tkm_to_var: shareTkmToVar, }, }; }); const sumShareTkmToVar = shareVarianceData.reduce( (sum, p) => sum + Number(p.varianceInput.share_tkm_to_var), 0 ); return shareVarianceData.map(p => ({ ...p, varianceInput: { actual_price: p.varianceInput.actual_price, abs_spot_var: p.varianceInput.abs_spot_var, token_milliseconds: p.varianceInput.token_milliseconds, share_tkm_to_var: p.varianceInput.share_tkm_to_var, sum_share_tkm_to_var: sumShareTkmToVar, }, })); } /** * Computes the final spot game results for players, including * winnings, top winners, and highest returns based on their predictions. * * @param data - List of SpotPlayer entries participating in the game * @param game - SpotGame containing game closing data * @param distribution - The SpotGame distribution * @returns An object containing player winnings, highest winnings, and largest returns */ function computeSpotWinnings( data: SpotPlayer[], game: SpotGame, distribution: PotDistribution ): ComputedData { // If no players, immediately return empty results if (data.length === 0) { return { players: [], streak: { winnings: [], returns: [], }, }; } // Step 1: Calculate total pot distribution and each player's variance and share based on spot prediction const players = data.map(serializeSpotPlayer); const computedPlayers = computeSpotPlayerVariance(players, game); // Step 3: Calculate winnings and percent return for each player const updatedPlayers = computedPlayers.map(player => { // Calculate normalized share of time/variance const shareTotalTimeVariance = player.varianceInput.sum_share_tkm_to_var > 0 ? player.varianceInput.share_tkm_to_var / player.varianceInput.sum_share_tkm_to_var : 0; const winningsRaw = shareTotalTimeVariance * distribution.winning_pot; const percentReturn = parseNumeric(player.f) > 0 ? ((winningsRaw - parseNumeric(player.f)) / parseNumeric(player.f)) * 100 : 0; const { varianceInput, ...rest } = player; return { ...rest, w: winningsRaw, r: percentReturn, }; }); // Step 4: Identify top 2% of players by winnings const totalPlayers = updatedPlayers.length; const topPlayersCount = Math.max(1, Math.ceil(totalPlayers * 0.02)); // Always at least 1 player const highestWinnings = [...updatedPlayers] .sort((a, b) => b.w - a.w) .slice(0, topPlayersCount); // Step 5: Identify top 2% of players by percent return const largestReturns = [...updatedPlayers] .sort((a, b) => b.r - a.r) .slice(0, topPlayersCount); // Step 6: Return results return { players: updatedPlayers, streak: { winnings: highestWinnings, returns: largestReturns, }, }; } export { computeRangeWinnings, computeSpotWinnings, computeRangePlayerVariance, computeSpotPlayerVariance, calculateSpotPotDistribution, calculateRangePotDistribution, };