metafide-surge
Version:
Metafide Surge Game Computation
513 lines (463 loc) • 15.7 kB
text/typescript
/* 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,
};