Skip to content

Instantly share code, notes, and snippets.

@ilap
Created November 13, 2022 00:28
Show Gist options
  • Save ilap/42864638691c7a956ac39589f8068a46 to your computer and use it in GitHub Desktop.
Save ilap/42864638691c7a956ac39589f8068a46 to your computer and use it in GitHub Desktop.
Cardano's RSS Simulation - Pledge Model
#!/usr/bin/env python3
# Python implementation of Lars's Haskell based `pledge_model`.
# https://github.com/brunjlar/pledging-model
#
# Install:
# python -m pip install numpy argparse
import random
import numpy as np
import argparse
def add_script_arguments(parser):
parser.add_argument("--n", nargs="?", type=int, default=3500, help="The number of players (natural number). Default is 3500.")
parser.add_argument("--k", nargs="+", type=int, default=100, help="The desired nr. of pools (natural number). Default is 500.")
parser.add_argument("--epoch", nargs="+", type=int, default=5, help="Days per epoch (natural number). Default is 5.")
parser.add_argument("--ada-total", nargs="+", type=int, default=31112483745, help="Total ADA in circulation (default: 31112483745)")
parser.add_argument("--ada-supply", nargs="+", type=int, default=45000000000, help="The max supply of ADA (default: 45000000000)")
parser.add_argument("--expansion", nargs="+", type=float, default=0.0012, help="The monetary expansion per epoch (default: 0.0012)")
parser.add_argument("--treasury", nargs="+", type=float, default=0.1, help="The treasury ratio (default: 0.1)")
parser.add_argument("--rate", nargs="+", type=float, default=0.5, help="The exchange rate ($/ada) (default: 0.5)")
parser.add_argument("--min-cost", nargs="+", type=int, default=0, help="The min cost per year ($) (default: 0)")
parser.add_argument("--scale", nargs="+", type=float, default=8684, help="The Weibull scale (default: 8684)")
parser.add_argument("--shape", nargs="+", type=float, default=2, help="The Weibull shape (default: 2)")
parser.add_argument("--pareto", nargs="+", type=float, default=1.16, help="Pareto alpha (default: 1.16)")
parser.add_argument("--whale", nargs="+", type=float, default=0.0005, help="The relative whale threshold (default: 0.0005)")
parser.add_argument("--a0", nargs="+", type=float, default=0.3, help="The pledge influence (default: 0.3)")
class Player:
def __init__(self, stake, cost):
self.stake = stake
self.cost = cost
def __str__(self):
return "Player(%f, %0.15f)" % (self.stake, self.cost)
class GameConfig:
def __init__(self):
self.n = 35000
self.k = 200
self.epoch = 5
self.total = 31112483745
self.supply = 45000000000
self.expansion = 0.0012
self.treasury = 0.1
self.rate = 0.08
self.minCostPerYear = 0
self.weibullScale = 8684
self.weibullShape = 2
self.paretoAlpha = 1.16
self.whaleThresholds = 0.0005
self.a0 = 0.3
cfg = GameConfig()
def paretoWeight(alpha, w):
rng = random.random()
# DEBUG: rng = float(w / 100)
return round(np.reciprocal(1 - rng) ** np.reciprocal(alpha))
def relativeToDollarsPerYear(r):
a = adaRewardsPerEpoch()
e = epochsPerYear()
x = cfg.rate
return r * a * e * x
z0 = lambda : 1 / cfg.k
stakeToAda = lambda s : s * cfg.total
stakeToUsd = lambda s: stakeToAda(s) * cfg.rate
def sampleRelCost():
n = cfg.n
sc = cfg.weibullScale
sh = cfg.weibullShape
mc = cfg.minCostPerYear
wb = map(lambda x: sc * x, np.random.weibull(sh, n))
cs = map(lambda x :mc if x <= mc else x, wb)
return map(dollarsPerYearToRelative, cs)
def dollarsPerYearToRelative(d):
e = epochsPerYear()
r = adaRewardsPerEpoch()
a = dollarsToAda(d)
return a / e / r
epochsPerYear = lambda: 365 / cfg.epoch
def adaRewardsPerEpoch():
t = cfg.treasury
e = cfg.expansion
s = cfg.supply
c = cfg.total
return (1 - t) * e * (s - c)
dollarsToAda = lambda d : d / cfg.rate
def satPoolRewards(lam):
z = 1 / cfg.k #z0()
a = cfg.a0
l = min(lam, z)
beta = z
return 1 / (1 + a) * (beta + l * a)
poolPotential = lambda p:satPoolRewards(p.stake) - p.cost
margin = lambda p, q: 1 - poolPotential(q) / poolPotential(p)
def operatorProfit(p, m):
r = satPoolRewards(p.stake)
pp = poolPotential(p)
z = z0()
x = m * r ; pp
r = (pp - x) * (p.stake / z)
return x + r
def mkPlayer():
a0 = cfg.a0
z = 1 / cfg.k
ws = list(map(lambda x: paretoWeight(cfg.paretoAlpha, x), list(range(1, cfg.n+1))))
# DEBUG: ws = list(range(1, cfg.n+1))
cs = list(sampleRelCost())
potential = lambda lam, c : (z + a0 * lam) / (1 + a0) - c
q = sum(ws)
wss = list(map(lambda w: w / q, ws))
wcs = list(zip(wss, cs))
pwcs = sorted(wcs, key = lambda x: -potential(x[0], x[1]), reverse = False)
return map(lambda w: Player(w[0], w[1]), pwcs)
def main():
print("Pledge Modeling")
parser = argparse.ArgumentParser(description="Lars' pledge modelling")
add_script_arguments(parser)
args = parser.parse_args()
players = list(mkPlayer())
nonWhales = list(filter(lambda p: p.stake < cfg.whaleThresholds, players)) # remove whales from list of players
z = z0()
spp = lambda p: [p] if p.stake < z else [Player(z, p.cost )] + spp(Player(p.stake - z, p.cost))
nonWhales1 = []
for player in nonWhales:
res = spp(player)
nonWhales1.extend(res)
k = cfg.k
operators1 = nonWhales1[:k+1] # k+1 nr. of nodes
loser = operators1[-1]
operators = operators1[:-1]
oms = map(lambda x: (x, margin(x, loser)), operators)
richest = max(operators, key=lambda x: x.stake)
poorest = min(operators, key=lambda x: x.stake)
middle = operators[k // 2]
def sybilProtectionStake(player):
k = cfg.k
mc = dollarsPerYearToRelative(cfg.minCostPerYear)
a0 = cfg.a0
l = player.stake
return (l - (player.cost - mc) * (1 + 1 / a0)) * k / 2
sybil = sybilProtectionStake(middle)
richestA = stakeToAda(richest.stake)
poorestA = stakeToAda(poorest.stake)
sybilA = stakeToAda(sybil)
richestD = stakeToUsd(richest.stake)
poorestD = stakeToUsd(poorest.stake)
sybilD = stakeToUsd(sybil)
rewards = adaRewardsPerEpoch()
e = epochsPerYear()
c = cfg.total
r = adaRewardsPerEpoch()
i = 0
print( "pool pledge (ada) cost per year ($) pool rewards per epoch (ada) potential pool profit per epoch (ada) margin operator profit per epoch (ada) ROI (percent))\n")
for (p1, m) in oms:
stake = stakeToAda(p1.stake)
cost = relativeToDollarsPerYear(p1.cost)
spr = r * satPoolRewards(p1.stake)
ppp = r * poolPotential(p1)
op = r * operatorProfit(p1, m)
roi = 100 * op * e / (p1.stake * c)
i += 1
print("%6d %11.0f %5.f %8.0f %8.0f %8.6f %8.0f %5.2f" % (i, stake, cost, spr, ppp, m, op, roi))
print()
print("number of ada holders: %11d" % cfg.n)
print("number of pools: %11d" % cfg.k)
print("days per epoch: %13.1f" % cfg.epoch)
print("ada in circulation: %11.0f" % cfg.total)
print("max supply of ada: %11.0f" % cfg.supply)
print("monetary expansion: %16.4f" % cfg.expansion)
print("treasury ratio: %16.4f" % cfg.treasury)
print("exchange rate ($/ada): %16.4f" % cfg.rate)
print("min cost per year ($): %11.0f" % cfg.minCostPerYear)
print("Weibull scale: %11.0f" % cfg.weibullScale)
print("Weibull shape: %14.2f" % cfg.weibullShape)
print("Pareto alpha: %14.2f" % cfg.paretoAlpha)
print("Whale threshold: %16.4f" % cfg.whaleThresholds)
print("pledge influence: %14.2f" % cfg.a0)
print()
print("number of whales: %11d" % (cfg.n - len(nonWhales)))
print("rewards per epoch (ada) : %11.0f" % rewards)
print("richest pool operator stake (ada) : %11.0f" % richestA)
print("poorest pool operator stake (ada) : %11.0f" % poorestA)
print("sybil attacker min stake (ada) : %11.0f" % sybilA)
print("richest pool operator stake ($) : %11.0f" % richestD)
print("poorest pool operator stake ($) : %11.0f" % poorestD)
print("sybil attacker min stake ($) : %11.0f" % sybilD)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment