Add script to calc apy.

This commit is contained in:
FiveMovesAhead 2024-12-29 03:09:49 +08:00
parent 74c31623b9
commit 2364377174
4 changed files with 221 additions and 64 deletions

112
tig-benchmarker/calc_apy.py Normal file
View File

@ -0,0 +1,112 @@
import numpy as np
import json
import requests
from common.structs import *
from common.calcs import *
API_URL = "https://mainnet-api.tig.foundation"
deposit = input("Enter deposit in TIG (leave blank to fetch deposit from your player_id): ")
if deposit != "":
lock_period = input("Enter number of weeks to lock (longer lock will have higher APY): ")
deposit = float(deposit)
lock_period = float(lock_period)
player_id = None
else:
player_id = input("Enter player_id: ").lower()
print("Fetching data...")
block = Block.from_dict(requests.get(f"{API_URL}/get-block?include_data").json()["block"])
opow_data = {
x["player_id"]: OPoW.from_dict(x)
for x in requests.get(f"{API_URL}/get-opow?block_id={block.id}").json()["opow"]
}
factors = {
benchmarker: {
**{
f: opow_data[benchmarker].block_data.num_qualifiers_by_challenge.get(f, 0)
for f in block.data.active_ids["challenge"]
},
"weighted_deposit": opow_data[benchmarker].block_data.delegated_weighted_deposit.to_float()
}
for benchmarker in opow_data
}
if player_id is None:
blocks_till_round_ends = block.config["rounds"]["blocks_per_round"] - (block.details.height % block.config["rounds"]["blocks_per_round"])
seconds_till_round_ends = blocks_till_round_ends * block.config["rounds"]["seconds_between_blocks"]
weighted_deposit = calc_weighted_deposit(deposit, seconds_till_round_ends, lock_period * 604800)
else:
player_data = Player.from_dict(requests.get(f"{API_URL}/get-player-data?player_id={player_id}&block_id={block.id}").json()["player"])
deposit = sum(x.to_float() for x in player_data.block_data.deposit_by_locked_period)
weighted_deposit = player_data.block_data.weighted_deposit.to_float()
for delegatee, fraction in player_data.block_data.delegatees.items():
factors[delegatee]["weighted_deposit"] -= fraction * weighted_deposit
total_factors = {
f: sum(factors[benchmarker][f] for benchmarker in opow_data)
for f in list(block.data.active_ids["challenge"]) + ["weighted_deposit"]
}
reward_shares = {
benchmarker: opow_data[benchmarker].block_data.reward_share
for benchmarker in opow_data
}
print("Optimising delegation by splitting into 100 chunks...")
chunk = weighted_deposit / 100
delegate = {}
for i in range(100):
print(f"Chunk {i + 1}: simulating delegation...")
total_factors["weighted_deposit"] += chunk
if len(delegate) == 10:
potential_delegatees = list(delegate)
else:
potential_delegatees = [benchmarker for benchmarker in opow_data if opow_data[benchmarker].block_data.self_deposit.to_float() >= 10000]
highest_apy_benchmarker = max(
potential_delegatees,
key=lambda delegatee: (
calc_influence({
benchmarker: {
f: (factors[benchmarker][f] + chunk * (benchmarker == delegatee and f == "weighted_deposit")) / total_factors[f]
for f in total_factors
}
for benchmarker in opow_data
}, block.config["opow"])[delegatee] *
reward_shares[delegatee] * chunk / (factors[delegatee]["weighted_deposit"] + chunk)
)
)
print(f"Chunk {i + 1}: best delegatee is {highest_apy_benchmarker}")
if highest_apy_benchmarker not in delegate:
delegate[highest_apy_benchmarker] = 0
delegate[highest_apy_benchmarker] += 1
factors[highest_apy_benchmarker]["weighted_deposit"] += chunk
influences = calc_influence({
benchmarker: {
f: factors[benchmarker][f] / total_factors[f]
for f in total_factors
}
for benchmarker in opow_data
}, block.config["opow"])
print("")
print("Optimised delegation split:")
reward_pool = block.config["rewards"]["distribution"]["opow"] * next(x["block_reward"] for x in reversed(block.config["rewards"]["schedule"]) if x["round_start"] <= block.details.round)
deposit_chunk = deposit / 100
total_reward = 0
for delegatee, num_chunks in delegate.items():
share_fraction = influences[delegatee] * reward_shares[delegatee] * (num_chunks * chunk) / factors[delegatee]["weighted_deposit"]
reward = share_fraction * reward_pool
total_reward += reward
apy = reward * block.config["rounds"]["blocks_per_round"] * 52 / (num_chunks * deposit_chunk)
print(f"{delegatee}: %delegated = {num_chunks}%, apy = {apy * 100:.2f}%")
print(f"average_apy = {total_reward * 10080 * 52 / deposit * 100:.2f}% on your deposit of {deposit} TIG")
print("")
print("To set this delegation split, run the following command:")
req = {"delegatees": {k: v / 100 for k, v in delegate.items()}}
print("API_KEY=<YOUR API KEY HERE>")
print(f"curl -H \"X-Api-Key: $API_KEY\" -X POST -d '{json.dumps(req)}' {API_URL}/set-delegatees")

View File

@ -1,68 +1,10 @@
import numpy as np
import json
import requests
from typing import List, Dict
from common.structs import *
from common.calcs import *
from copy import deepcopy
def calc_influence(fractions, opow_config) -> Dict[str, float]:
benchmarkers = list(fractions)
factors = list(next(iter(fractions.values())))
num_challenges = len(factors) - 1
avg_qualifier_fractions = {
benchmarker: sum(
fractions[benchmarker][f]
for f in factors
if f != "weighted_deposit"
) / num_challenges
for benchmarker in benchmarkers
}
deposit_fraction_cap = {
benchmarker: avg_qualifier_fractions[benchmarker] * opow_config["max_deposit_to_qualifier_ratio"]
for benchmarker in benchmarkers
}
capped_fractions = {
benchmarker: {
**fractions[benchmarker],
"weighted_deposit": min(
fractions[benchmarker]["weighted_deposit"],
deposit_fraction_cap[benchmarker]
)
}
for benchmarker in benchmarkers
}
avg_fraction = {
benchmarker: np.mean(list(capped_fractions[benchmarker].values()))
for benchmarker in benchmarkers
}
var_fraction = {
benchmarker: np.var(list(capped_fractions[benchmarker].values()))
for benchmarker in benchmarkers
}
imbalance = {
benchmarker: (var_fraction[benchmarker] / np.square(avg_fraction[benchmarker]) / num_challenges) if avg_fraction[benchmarker] > 0 else 0
for benchmarker in benchmarkers
}
imbalance_penalty = {
benchmarker: 1.0 - np.exp(-opow_config["imbalance_multiplier"] * imbalance[benchmarker])
for benchmarker in benchmarkers
}
weighted_avg_fraction = {
benchmarker: ((avg_qualifier_fractions[benchmarker] * num_challenges) + capped_fractions[benchmarker]["weighted_deposit"] * opow_config["deposit_multiplier"]) / (num_challenges + opow_config["deposit_multiplier"])
for benchmarker in benchmarkers
}
unormalised_influence = {
benchmarker: weighted_avg_fraction[benchmarker] * (1.0 - imbalance_penalty[benchmarker])
for benchmarker in benchmarkers
}
total = sum(unormalised_influence.values())
influence = {
benchmarker: unormalised_influence[benchmarker] / total
for benchmarker in benchmarkers
}
return influence
API_URL = "https://mainnet-api.tig.foundation"
player_id = input("Enter player_id: ").lower()
@ -140,12 +82,12 @@ print(f"reward = {reward['only_self_deposit']:.4f} TIG per block")
print("")
print("Scenario 2 (current self + delegated deposit)")
print(f"%weighted_deposit = {fractions[player_id]['weighted_deposit'] * 100:.2f}%")
print(f"reward = {reward['current']:.4f} TIG per block ({reward['current'] / reward['only_self_deposit'] * 100 - 100:.2f}% difference*)")
print(f"reward = {reward['current']:.4f} TIG per block (recommended max reward_share = {100 - reward['only_self_deposit'] / reward['current'] * 100:.2f}%*)")
print("")
print(f"Scenario 3 (self + delegated deposit at parity)")
print(f"%weighted_deposit = average %qualifiers = {average_fraction_of_qualifiers * 100:.2f}%")
print(f"reward = {reward['at_parity']:.4f} TIG per block ({reward['at_parity'] / reward['only_self_deposit'] * 100 - 100:.2f}% difference*)")
print(f"reward = {reward['at_parity']:.4f} TIG per block (recommended max reward_share = {100 - reward['only_self_deposit'] / reward['at_parity'] * 100:.2f}%*)")
print("")
print("*These are percentage differences in reward compared with relying only on self-deposit (Scenario 1).")
print("*Recommend not setting reward_share above the max. You will not benefit from delegation (earn the same as Scenario 1 with zero delegation).")
print("")
print("Note: the imbalance penalty is such that your reward increases at a high rate when moving up to parity")

View File

@ -0,0 +1,102 @@
import numpy as np
from typing import List, Dict
def calc_influence(fractions: Dict[str, Dict[str, float]], opow_config: dict) -> Dict[str, float]:
"""
Calculate the influence of each benchmarker based on their fractions and the OPoW configuration.
Args:
fractions: A dictionary of dictionaries, mapping benchmarker_ids to their fraction of each factor (challenges & weighted_deposit).
opow_config: A dictionary containing configuration parameters for the calculation.
Returns:
Dict[str, float]: A dictionary mapping each benchmarker_id to their calculated influence.
"""
benchmarkers = list(fractions)
factors = list(next(iter(fractions.values())))
num_challenges = len(factors) - 1
avg_qualifier_fractions = {
benchmarker: sum(
fractions[benchmarker][f]
for f in factors
if f != "weighted_deposit"
) / num_challenges
for benchmarker in benchmarkers
}
deposit_fraction_cap = {
benchmarker: avg_qualifier_fractions[benchmarker] * opow_config["max_deposit_to_qualifier_ratio"]
for benchmarker in benchmarkers
}
capped_fractions = {
benchmarker: {
**fractions[benchmarker],
"weighted_deposit": min(
fractions[benchmarker]["weighted_deposit"],
deposit_fraction_cap[benchmarker]
)
}
for benchmarker in benchmarkers
}
avg_fraction = {
benchmarker: np.mean(list(capped_fractions[benchmarker].values()))
for benchmarker in benchmarkers
}
var_fraction = {
benchmarker: np.var(list(capped_fractions[benchmarker].values()))
for benchmarker in benchmarkers
}
imbalance = {
benchmarker: (var_fraction[benchmarker] / np.square(avg_fraction[benchmarker]) / num_challenges) if avg_fraction[benchmarker] > 0 else 0
for benchmarker in benchmarkers
}
imbalance_penalty = {
benchmarker: 1.0 - np.exp(-opow_config["imbalance_multiplier"] * imbalance[benchmarker])
for benchmarker in benchmarkers
}
weighted_avg_fraction = {
benchmarker: ((avg_qualifier_fractions[benchmarker] * num_challenges) + capped_fractions[benchmarker]["weighted_deposit"] * opow_config["deposit_multiplier"]) / (num_challenges + opow_config["deposit_multiplier"])
for benchmarker in benchmarkers
}
unormalised_influence = {
benchmarker: weighted_avg_fraction[benchmarker] * (1.0 - imbalance_penalty[benchmarker])
for benchmarker in benchmarkers
}
total = sum(unormalised_influence.values())
influence = {
benchmarker: unormalised_influence[benchmarker] / total
for benchmarker in benchmarkers
}
return influence
def calc_weighted_deposit(deposit: float, seconds_till_round_end: int, lock_seconds: int) -> float:
"""
Calculate weighted deposit
Args:
deposit: Amount to deposit
seconds_till_round_end: Seconds remaining in current round
lock_seconds: Total lock duration in seconds
Returns:
Weighted deposit
"""
weighted_deposit = 0
if lock_seconds <= 0:
return weighted_deposit
# Calculate first chunk (partial week)
weighted_deposit += deposit * min(seconds_till_round_end, lock_seconds) // lock_seconds
remaining_seconds = lock_seconds - min(seconds_till_round_end, lock_seconds)
weight = 2
while remaining_seconds > 0:
chunk_seconds = min(remaining_seconds, 604800)
chunk = deposit * chunk_seconds // lock_seconds
weighted_deposit += chunk * weight
remaining_seconds -= chunk_seconds
weight = min(weight + 1, 26)
return weighted_deposit

View File

@ -201,6 +201,7 @@ class OPoWBlockData(FromDict):
num_qualifiers_by_challenge: Dict[str, int]
cutoff: int
delegated_weighted_deposit: PreciseNumber
self_deposit: PreciseNumber
delegators: Set[str]
reward_share: float
imbalance: PreciseNumber
@ -221,13 +222,13 @@ class PlayerDetails(FromDict):
class PlayerState(FromDict):
total_fees_paid: PreciseNumber
available_fee_balance: PreciseNumber
delegatee: Optional[dict]
delegatees: Optional[dict]
votes: dict
reward_share: Optional[dict]
@dataclass
class PlayerBlockData(FromDict):
delegatee: Optional[str]
delegatees: Dict[str, float]
reward_by_type: Dict[str, PreciseNumber]
deposit_by_locked_period: List[PreciseNumber]
weighted_deposit: PreciseNumber