diff --git a/ct-app/core/core.py b/ct-app/core/core.py index a771cbc6..99538757 100644 --- a/ct-app/core/core.py +++ b/ct-app/core/core.py @@ -1,6 +1,8 @@ import asyncio +from copy import deepcopy from celery import Celery +from core.model.economic_model_sigmoid import EconomicModelSigmoid from prometheus_client import Gauge from .components.baseclass import Base @@ -17,7 +19,8 @@ from .components.parameters import Parameters from .components.utils import Utils from .model.address import Address -from .model.economic_model import EconomicModel +from .model.economic_model_legacy import EconomicModelLegacy +from .model.budget import Budget from .model.peer import Peer from .model.subgraph_entry import SubgraphEntry from .model.subgraph_type import SubgraphType @@ -53,6 +56,10 @@ def __init__(self): self.nodes = list[Node]() + self.budget: Budget = None + self.legacy_model: EconomicModelSigmoid = None + self.sigmoid_model: EconomicModelSigmoid = None + self.tasks = set[asyncio.Task]() self.connected = LockedVar("connected", False) @@ -83,6 +90,14 @@ def post_init(self, nodes: list[Node], params: Parameters): for node in self.nodes: node.params = params + self.budget = Budget.fromParameters(self.params.economicModel.budget) + + self.legacy_model = EconomicModelLegacy.fromParameters(self.params.economicModel.legacy) + self.sigmoid_model = EconomicModelSigmoid.fromParameters(self.params.economicModel.sigmoid) + + self.legacy_model.budget = deepcopy(self.budget) + self.sigmoid_model.budget = deepcopy(self.budget) + self._safe_subgraph_url = SubgraphURL( self.params.subgraph.deployerKey, self.params.subgraph.safesBalance ) @@ -320,34 +335,33 @@ async def apply_economic_model(self): f"Excluded non-nft-holders with stake < {threshold} ({len(excluded)} entries)." ) - model = EconomicModel.fromParameters(self.params.economicModel) + low_stake_addresses = [ + peer.address + for peer in eligibles + if peer.split_stake < self.model.coefficients.l + ] + excluded = Utils.excludeElements(eligibles, low_stake_addresses) + self.debug(f"Excluded nodes with low stake ({len(excluded)} entries).") redeemed_rewards = await self.peer_rewards.get() for peer in eligibles: - peer.economic_model = model + peer.economic_model = deepcopy(self.model) peer.economic_model.coefficients.c += redeemed_rewards.get(peer.address.address,0.0) - peer.max_apr = self.params.economicModel.maxAPRPercentage - self.debug("Assigned economic model to eligible nodes.") + self.info(f"Assigned economic model to eligible nodes. ({len(eligibles)} entries).") - excluded = Utils.rewardProbability(eligibles) - self.debug(f"Excluded nodes with low stakes ({len(excluded)} entries).") - - self.info(f"Eligible nodes ({len(eligibles)} entries).") - - self.debug(f"final eligible list {[el.address.id for el in eligibles]}") + self.debug(f"Final eligible list {[el.address.id for el in eligibles]}") await self.eligible_list.set(eligibles) # set prometheus metrics - DISTRIBUTION_DELAY.set(model.delay_between_distributions) + DISTRIBUTION_DELAY.set(self.model.delay_between_distributions) NEXT_DISTRIBUTION_EPOCH.set( - Utils.nextEpoch(model.delay_between_distributions).timestamp() + Utils.nextEpoch(self.model.delay_between_distributions).timestamp() ) ELIGIBLE_PEERS_COUNTER.set(len(eligibles)) for peer in eligibles: - APR_PER_PEER.labels(peer.address.id).set(peer.apr_percentage) JOBS_PER_PEER.labels(peer.address.id).set(peer.message_count_for_reward) PEER_SPLIT_STAKE.labels(peer.address.id).set(peer.split_stake) PEER_SAFE_COUNT.labels(peer.address.id).set(peer.safe_address_count) @@ -357,13 +371,8 @@ async def apply_economic_model(self): @flagguard @formalin("Distributing rewards") async def distribute_rewards(self): - model = EconomicModel.fromGCPFile( - self.params.gcp.bucket, self.params.economicModel.filename - ) - - delay = Utils.nextDelayInSeconds(model.delay_between_distributions) + delay = Utils.nextDelayInSeconds(self.budget.delay_between_distributions) self.debug(f"Waiting {delay} seconds for next distribution.") - await asyncio.sleep(delay) min_peers = self.params.distribution.minEligiblePeers @@ -392,11 +401,14 @@ async def distribute_rewards(self): app.autodiscover_tasks(force=True) for peer in peers: + legacy_count = self.legacy_model.message_count_for_reward(peer.split_stake) + sigmoid_count = self.sigmoid_model.message_count_for_reward(peer.split_stake) + Utils.taskSendMessage( app, peer.address.id, - peer.message_count_for_reward, - peer.economic_model.budget.ticket_price, + legacy_count + sigmoid_count, + self.budget.ticket_price, task_name=self.params.rabbitmq.taskName, ) self.info(f"Distributed rewards to {len(peers)} peers.") diff --git a/ct-app/core/model/budget.py b/ct-app/core/model/budget.py new file mode 100644 index 00000000..a2db2459 --- /dev/null +++ b/ct-app/core/model/budget.py @@ -0,0 +1,69 @@ +from core.components.parameters import Parameters +from prometheus_client import Gauge + +BUDGET_PERIOD = Gauge("budget_period", "Budget period for the economic model") +DISTRIBUTIONS_PER_PERIOD = Gauge("dist_freq", "Number of expected distributions") +TICKET_PRICE = Gauge("ticket_price", "Ticket price") +TICKET_WINNING_PROB = Gauge("ticket_winning_prob", "Ticket winning probability") + +class Budget: + def __init__( + self, + period: float, + distribution_per_period: float, + ticket_price: float, + winning_probability: float, + ): + self.period = period + self.distribution_per_period = distribution_per_period + self.ticket_price = ticket_price + self.winning_probability = winning_probability + + @property + def period(self): + return self._period + + @property + def distribution_per_period(self): + return self._distribution_per_period + + @property + def ticket_price(self): + return self._ticket_price + + @property + def winning_probability(self): + return self._winning_probability + + @period.setter + def period(self, value): + self._period = value + BUDGET_PERIOD.set(value) + + @distribution_per_period.setter + def distribution_per_period(self, value): + self._distribution_per_period = value + DISTRIBUTIONS_PER_PERIOD.set(value) + + @ticket_price.setter + def ticket_price(self, value): + self._ticket_price = value + TICKET_PRICE.set(value) + + @winning_probability.setter + def winning_probability(self, value): + self._winning_probability = value + TICKET_WINNING_PROB.set(value) + + @classmethod + def fromParameters(cls, parameters: Parameters): + return cls( + parameters.period, + parameters.countsInPeriod, + parameters.ticketPrice, + parameters.winningProbability, + ) + + @property + def delay_between_distributions(self): + return self.period / self.distribution_per_period diff --git a/ct-app/core/model/economic_model.py b/ct-app/core/model/economic_model.py deleted file mode 100644 index 2175431c..00000000 --- a/ct-app/core/model/economic_model.py +++ /dev/null @@ -1,164 +0,0 @@ -from core.components.parameters import Parameters -from prometheus_client import Gauge - -BUDGET = Gauge("budget", "Budget for the economic model") -BUDGET_PERIOD = Gauge("budget_period", "Budget period for the economic model") -DISTRIBUTIONS_PER_PERIOD = Gauge("dist_freq", "Number of expected distributions") -TICKET_PRICE = Gauge("ticket_price", "Ticket price") -TICKET_WINNING_PROB = Gauge("ticket_winning_prob", "Ticket winning probability") - - -class Equation: - def __init__(self, formula: str, condition: str): - self.formula = formula - self.condition = condition - - @classmethod - def fromParameters(cls, parameters: Parameters): - return cls(parameters.formula, parameters.condition) - -class Equations: - def __init__(self, f_x: Equation, g_x: Equation): - self.f_x = f_x - self.g_x = g_x - - @classmethod - def fromParameters(cls, parameters: Parameters): - return cls( - Equation.fromParameters(parameters.fx), - Equation.fromParameters(parameters.gx), - ) - - -class Coefficients: - def __init__(self, a: float, b: float, c: float, l: float): # noqa: E741 - self.a = a - self.b = b - self.c = c - self.l = l - - @classmethod - def fromParameters(cls, parameters: Parameters): - return cls( - parameters.a, - parameters.b, - parameters.c, - parameters.l, - ) - - -class Budget: - def __init__( - self, - amount: float, - period: float, - s: float, - distribution_per_period: float, - ticket_price: float, - winning_probability: float, - ): - self.amount = amount - self.period = period - self.s = s - self.distribution_per_period = distribution_per_period - self.ticket_price = ticket_price - self.winning_probability = winning_probability - - @property - def amount(self): - return self._amount - - @property - def period(self): - return self._period - - @property - def distribution_per_period(self): - return self._distribution_per_period - - @property - def ticket_price(self): - return self._ticket_price - - @property - def winning_probability(self): - return self._winning_probability - - @amount.setter - def amount(self, value): - self._amount = value - BUDGET.set(value) - - @period.setter - def period(self, value): - self._period = value - BUDGET_PERIOD.set(value) - - @distribution_per_period.setter - def distribution_per_period(self, value): - self._distribution_per_period = value - DISTRIBUTIONS_PER_PERIOD.set(value) - - @ticket_price.setter - def ticket_price(self, value): - self._ticket_price = value - TICKET_PRICE.set(value) - - @winning_probability.setter - def winning_probability(self, value): - self._winning_probability = value - TICKET_WINNING_PROB.set(value) - - @classmethod - def fromParameters(cls, parameters: Parameters): - return cls( - parameters.amount, - parameters.period, - parameters.s, - parameters.countsInPeriod, - parameters.ticketPrice, - parameters.winningProbability, - ) - - @property - def delay_between_distributions(self): - return self.period / self.distribution_per_period - - -class EconomicModel: - def __init__( - self, equations: Equations, coefficients: Coefficients, budget: Budget - ): - """ - Initialisation of the class. - """ - self.equations = equations - self.coefficients = coefficients - self.budget = budget - - def transformed_stake(self, stake: float): - func = self.equations.f_x - - # convert parameters attribute to dictionary - kwargs = vars(self.coefficients) - kwargs.update({"x": stake}) - - if not eval(func.condition, kwargs): - func = self.equations.g_x - - return eval(func.formula, kwargs) - - @property - def delay_between_distributions(self): - return self.budget.delay_between_distributions - - @classmethod - def fromParameters(cls, parameters: Parameters): - return EconomicModel( - Equations.fromParameters(parameters.equations), - Coefficients.fromParameters(parameters.coefficients), - Budget.fromParameters(parameters.budget), - ) - - def __repr__(self): - return f"EconomicModel({self.equations}, {self.coefficients}, {self.budget})" diff --git a/ct-app/core/model/economic_model_legacy.py b/ct-app/core/model/economic_model_legacy.py new file mode 100644 index 00000000..e16e61d6 --- /dev/null +++ b/ct-app/core/model/economic_model_legacy.py @@ -0,0 +1,87 @@ +from core.components.parameters import Parameters +from .budget import Budget + +class Equation: + def __init__(self, formula: str, condition: str): + self.formula = formula + self.condition = condition + + @classmethod + def fromParameters(cls, parameters: Parameters): + return cls(parameters.formula, parameters.condition) + +class Equations: + def __init__(self, f_x: Equation, g_x: Equation): + self.f_x = f_x + self.g_x = g_x + + @classmethod + def fromParameters(cls, parameters: Parameters): + return cls( + Equation.fromParameters(parameters.fx), + Equation.fromParameters(parameters.gx), + ) + + +class Coefficients: + def __init__(self, a: float, b: float, c: float, l: float): # noqa: E741 + self.a = a + self.b = b + self.c = c + self.l = l + + @classmethod + def fromParameters(cls, parameters: Parameters): + return cls( + parameters.a, + parameters.b, + parameters.c, + parameters.l, + ) + + +class EconomicModelLegacy: + def __init__( + self, equations: Equations, coefficients: Coefficients, proportion: float, apr: float + ): + """ + Initialisation of the class. + """ + self.equations = equations + self.coefficients = coefficients + self.proportion = proportion + self.apr = apr + self.budget: Budget = None + + def transformed_stake(self, stake: float): + func = self.equations.f_x + + # convert parameters attribute to dictionary + kwargs = vars(self.coefficients) + kwargs.update({"x": stake}) + + if not eval(func.condition, kwargs): + func = self.equations.g_x + + return eval(func.formula, kwargs) + + def message_count_for_reward(self, stake: float): + """ + Calculate the message count for the reward. + """ + rewards = self.apr / 12 * self.transformed_stake(stake) + denominator = self.budget.ticket_price * self.budget.winning_probability + + return round(rewards / denominator * self.economic_model.proportion) + + @classmethod + def fromParameters(cls, parameters: Parameters): + return cls( + Equations.fromParameters(parameters.equations), + Coefficients.fromParameters(parameters.coefficients), + parameters.proportion, + parameters.apr + ) + + def __repr__(self): + return f"EconomicModelLegacy({self.equations}, {self.coefficients}, {self.budget})" diff --git a/ct-app/core/model/economic_model_sigmoid.py b/ct-app/core/model/economic_model_sigmoid.py new file mode 100644 index 00000000..11a1a3ac --- /dev/null +++ b/ct-app/core/model/economic_model_sigmoid.py @@ -0,0 +1,79 @@ +from core.components.parameters import Parameters +from .budget import Budget +from math import log, pow + +class Bucket: + def __init__(self, name: str, flatness: float, skewness: float, upperbound: float): + self.name = name + self.flatness = flatness + self.skewness = skewness + self.upperbound = upperbound + + def apr(self, x: float): + """ + Calculate the APR for the bucket. + """ + try: + return log(pow(self.upperbound / x, self.skewness) - 1) / self.flatness + except ValueError as e: + raise e + except ZeroDivisionError as e: + raise ValueError(e) + except OverflowError as e: + raise ValueError(e) + + @classmethod + def fromParameters(cls, name: str, parameters: Parameters): + return cls( + name, + parameters.flatness, + parameters.skewness, + parameters.upperbound, + ) + +class EconomicModelSigmoid: + def __init__(self, offset: float, buckets: list[Bucket], max_apr: float, proportion: float): + """ + Initialisation of the class. + """ + self.offset = offset + self.buckets = buckets + self.max_apr = max_apr + self.proportion = proportion + self.budget: Budget = None + + def apr(self, xs: list[float], max_apr: float = None): + """ + Calculate the APR for the economic model. + """ + apr = sum(b.apr(x) for b, x in zip(self.buckets, xs)) + self.offset + + if max_apr is not None: + apr = min(apr, max_apr) + + return apr + + def message_count_for_reward(self, stake: float, xs: list[float]): + """ + Calculate the message count for the reward. + """ + apr = self.apr(xs, self.max_apr) + + rewards = apr / 100.0 / 12.0 * stake + denominator = self.budget.ticket_price * self.budget.winning_probability + + return round(rewards / denominator * self.proportion) + + @classmethod + def fromParameters(cls, parameters: Parameters): + bucket_names = vars(parameters.buckets) + + return cls( + parameters.offset, + [Bucket.fromParameters(name, getattr(parameters.buckets, name)) for name in bucket_names], + parameters.maxAPRPercentage, + parameters.proportion, + ) + + def __repr__(self): + return f"EconomicModel({self.offset}, {self.buckets}, {self.budget})" diff --git a/ct-app/core/model/peer.py b/ct-app/core/model/peer.py index 31e9efb7..2ffb2b0e 100644 --- a/ct-app/core/model/peer.py +++ b/ct-app/core/model/peer.py @@ -15,10 +15,7 @@ def __init__(self, id: str, address: str, version: str): self._safe_address_count = None - self.economic_model = None - self.reward_probability = None - - self.max_apr = float("inf") + # self.economic_model = None def version_is_old(self, min_version: str or Version) -> bool: if isinstance(min_version, str): @@ -52,22 +49,6 @@ def safe_address_count(self) -> int: def safe_address_count(self, value: int): self._safe_address_count = value - @property - def transformed_stake(self) -> float: - if self.economic_model is None: - raise ValueError("Economic model not set") - - return self.economic_model.transformed_stake(self.split_stake) - - @property - def total_balance(self) -> float: - if self.safe_balance is None: - raise ValueError("Safe balance not set") - if self.channel_balance is None: - raise ValueError("Channel balance not set") - - return float(self.channel_balance) + float(self.safe_balance) - @property def split_stake(self) -> float: if self.safe_balance is None: @@ -80,83 +61,7 @@ def split_stake(self) -> float: return float(self.safe_balance) / float(self.safe_address_count) + float( self.channel_balance ) - - @property - def has_low_stake(self) -> bool: - if self.economic_model is None: - raise ValueError("Economic model not set") - - return self.split_stake < self.economic_model.parameters.l - - @property - def max_expected_reward(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - if self.reward_probability is None: - raise ValueError("Reward probability not set") - - return self.reward_probability * self.economic_model.budget.amount - - @property - def expected_reward(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - - return ( - self.apr_percentage - / 100.0 - * self.split_stake - * self.economic_model.budget.period - / (60 * 60 * 24 * 365) - ) - - @property - def airdrop_reward(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - - return self.expected_reward * (1 - self.economic_model.budget.s) - - @property - def protocol_reward(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - - return self.expected_reward * self.economic_model.budget.s - - @property - def protocol_reward_per_distribution(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - - return self.protocol_reward / self.economic_model.budget.distribution_per_period - - @property - def message_count_for_reward(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - - budget = self.economic_model.budget - denominator = budget.ticket_price * budget.winning_probability - - return round(self.protocol_reward_per_distribution / denominator) - - @property - def apr_percentage(self): - if self.economic_model is None: - raise ValueError("Economic model not set") - - seconds_in_year = 60 * 60 * 24 * 365 - period = self.economic_model.budget.period - - apr = ( - (self.max_expected_reward / self.split_stake) - * (seconds_in_year / period) - * 100 - ) - - return min(self.max_apr, apr) - + @property def complete(self) -> bool: # check that none of the attributes are None @@ -177,17 +82,8 @@ def attributesToExport(cls): "channel_balance", "safe_address", "safe_balance", - "total_balance", "safe_address_count", "split_stake", - "transformed_stake", - "apr_percentage", - "max_expected_reward", - "expected_reward", - "airdrop_reward", - "protocol_reward", - "protocol_reward_per_distribution", - "message_count_for_reward", ] @classmethod diff --git a/ct-app/scripts/core_production_config.yaml b/ct-app/scripts/core_production_config.yaml index 759b64f2..d20520bb 100644 --- a/ct-app/scripts/core_production_config.yaml +++ b/ct-app/scripts/core_production_config.yaml @@ -36,29 +36,45 @@ flags: # ============================================================================= economicModel: minSafeAllowance: -1 - maxAPRPercentage: 15.0 NFTThreshold: ~ - coefficients: - a: 1 - b: 1 - c: 3 - l: 0 - - equations: - fx: - formula: "a * x" - condition: "l <= x <= c" - gx: - formula: "a * c + (x - c) ** (1 / b)" - condition: "x > c" + legacy: + proportion: 0.9 + apr: 12.0 + + coefficients: + a: 1 + b: 1 + c: 3 + l: 0 + + equations: + fx: + formula: "a * x" + condition: "l <= x <= c" + gx: + formula: "a * c + (x - c) ** (1 / b)" + condition: "x > c" + sigmoid: + proportion: 0.1 + maxAPRPercentage: 15.0 + offset: 10 + + buckets: + economicSecurity: + flatness: 1.65 + skewness: 1.50 + upperbound: 0.5 + networkCapacity: + flatness: 10.0 + skewness: 2.75 + upperbound: 1.0 + budget: - amount: 400 - period: 1200 - s: 0.25 - countsInPeriod: 1 - ticketPrice: 0.5 # deprecated + period: 2628000 + countsInPeriod: 365 + ticketPrice: 0.03 # deprecated winningProbability: 1 # ============================================================================= diff --git a/ct-app/scripts/core_staging_config.yaml b/ct-app/scripts/core_staging_config.yaml index 18371b0f..baeb11bf 100644 --- a/ct-app/scripts/core_staging_config.yaml +++ b/ct-app/scripts/core_staging_config.yaml @@ -35,27 +35,43 @@ flags: # ============================================================================= economicModel: minSafeAllowance: -1 - maxAPRPercentage: 15.0 NFTThreshold: ~ - coefficients: - a: 1 - b: 1 - c: 3 - l: 0 - - equations: - fx: - formula: "a * x" - condition: "l <= x <= c" - gx: - formula: "a * c + (x - c) ** (1 / b)" - condition: "x > c" - + legacy: + proportion: 0.9 + apr: 12.0 + + coefficients: + a: 1 + b: 1 + c: 3 + l: 0 + + equations: + fx: + formula: "a * x" + condition: "l <= x <= c" + gx: + formula: "a * c + (x - c) ** (1 / b)" + condition: "x > c" + + sigmoid: + proportion: 0.1 + maxAPRPercentage: 15.0 + offset: 10 + + buckets: + economicSecurity: + flatness: 1.65 + skewness: 1.50 + upperbound: 0.5 + networkCapacity: + flatness: 10.0 + skewness: 2.75 + upperbound: 1.0 + budget: - amount: 400 period: 1200 - s: 0.25 countsInPeriod: 1 ticketPrice: 0.5 # deprecated winningProbability: 1 diff --git a/ct-app/test/model/test_economic_model_legacy.py b/ct-app/test/model/test_economic_model_legacy.py new file mode 100644 index 00000000..08aad721 --- /dev/null +++ b/ct-app/test/model/test_economic_model_legacy.py @@ -0,0 +1,7 @@ +from core.model.budget import Budget +from core.model.economic_model_legacy import EconomicModelLegacy +from core.components.parameters import Parameters +import pytest + +def test_init_class(): + pass \ No newline at end of file diff --git a/ct-app/test/model/test_economic_model_sigmoid.py b/ct-app/test/model/test_economic_model_sigmoid.py new file mode 100644 index 00000000..7f9f88cf --- /dev/null +++ b/ct-app/test/model/test_economic_model_sigmoid.py @@ -0,0 +1,85 @@ +from core.model.budget import Budget +from core.model.economic_model_sigmoid import Bucket, EconomicModelSigmoid +from core.components.parameters import Parameters +import pytest + +def test_init_class(): + config = { + "sigmoid": { + "proportion": 0.1, + "maxAPRPercentage": 15.0, + "offset": 10.0, + "buckets": { + "bucket_1": { + "flatness": 1, + "skewness": 2, + "upperbound": 3, + }, + "bucket_2": { + "flatness": 4, + "skewness": 5, + "upperbound": 6, + }, + } + } + } + params = Parameters() + params.parse(config) + + economic_model = EconomicModelSigmoid.fromParameters(params.sigmoid) + bucket = economic_model.buckets[0] + + assert len(economic_model.buckets) == len(config["sigmoid"]["buckets"]) + + for bucket in economic_model.buckets: + assert bucket.flatness == config["sigmoid"]["buckets"][bucket.name]["flatness"] + assert bucket.skewness == config["sigmoid"]["buckets"][bucket.name]["skewness"] + assert bucket.upperbound == config["sigmoid"]["buckets"][bucket.name]["upperbound"] + +def test_values_mid_range(): + assert EconomicModelSigmoid(0, [Bucket("bucket_1", 1, 1, 1), Bucket("bucket_2", 1, 1, 0.5)], + 20.0, 1).apr([0.5, 0.25]) == 0 + + assert EconomicModelSigmoid(10.0, [Bucket("bucket_1", 1, 1, 1), Bucket("bucket_2", 1, 1, 0.5)], + 20.0, 1).apr([0.5, 0.25]) == 10 + +def test_value_above_mid_range(): + assert EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 1)], 20.0, 1).apr([0.75]) < 0 + +def test_value_below_mid_range(): + assert EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 1)], 20.0, 1).apr([0.25]) > 0 + +def test_apr_composition(): + assert EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 1)], 20.0, 1).apr([0.25]) * 2 == EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 1)]*2, 20.0, 1).apr([0.25]*2) + + assert EconomicModelSigmoid(1, [Bucket("bucket", 1, 1, 1)], 20.0, 1).apr([0.25]) * 2 != EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 1)]*2, 20.0, 1).apr([0.25]*2) + +def test_out_of_bounds_values(): + with pytest.raises(ValueError): + EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 0.5)], 20.0, 1).apr([0.5]) + + with pytest.raises(ValueError): + EconomicModelSigmoid(0, [Bucket("bucket", 1, 1, 0.5)], 20.0, 1).apr([0]) + +def test_bucket_apr(): + bucket = Bucket("bucket", 1, 1, 0.5) + + with pytest.raises(ValueError): + bucket.apr(0) + + assert bucket.apr(0.125) > 0 + assert bucket.apr(0.25) == 0 + assert bucket.apr(0.375) < 0 + + with pytest.raises(ValueError): + bucket.apr(0.5) + +def test_economic_model_message_count_for_reward(): + stake = 100 + model = EconomicModelSigmoid(10.0, + [Bucket("bucket_1", 1, 1, 1), Bucket("bucket_2", 1, 1, 0.5)], + 20.0, 1) + model.budget = Budget(60, 1, 1, 1) + + assert model.apr([0.5, 0.25]) == 10 + assert model.message_count_for_reward(stake, [0.5, 0.25]) == round(model.apr([0.5, 0.25]) / 100.0 / 12 * stake) \ No newline at end of file