diff --git a/tig-benchmarker/calc_apy.py b/tig-benchmarker/calc_apy.py new file mode 100644 index 0000000..a1cf1a8 --- /dev/null +++ b/tig-benchmarker/calc_apy.py @@ -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=") +print(f"curl -H \"X-Api-Key: $API_KEY\" -X POST -d '{json.dumps(req)}' {API_URL}/set-delegatees") \ No newline at end of file diff --git a/tig-benchmarker/calc_rewards.py b/tig-benchmarker/calc_reward_share.py similarity index 58% rename from tig-benchmarker/calc_rewards.py rename to tig-benchmarker/calc_reward_share.py index 1a28989..853053a 100644 --- a/tig-benchmarker/calc_rewards.py +++ b/tig-benchmarker/calc_reward_share.py @@ -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") \ No newline at end of file diff --git a/tig-benchmarker/common/calcs.py b/tig-benchmarker/common/calcs.py new file mode 100644 index 0000000..3f47230 --- /dev/null +++ b/tig-benchmarker/common/calcs.py @@ -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 \ No newline at end of file diff --git a/tig-benchmarker/common/structs.py b/tig-benchmarker/common/structs.py index c2d29f1..4a69547 100644 --- a/tig-benchmarker/common/structs.py +++ b/tig-benchmarker/common/structs.py @@ -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