Module src.TokenLab.simulationcomponents.tokeneconomyclasses
Created on Thu Nov 3 11:15:50 2022
@author: stylianoskampakis
Expand source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Thu Nov 3 11:15:50 2022
@author: stylianoskampakis
"""
import numpy as np
import scipy
from scipy.stats import binom,norm
from typing import Union,TypeVar,Callable
from typing import List, Dict,Union,Tuple
from baseclasses import Controller,TokenEconomy
from agentpoolclasses import AgentPool
from collections import OrderedDict
import pandas as pd
import copy
#import seaborn as sns
from tqdm import tqdm
from matplotlib import pyplot as plt
from pricingclasses import *
import matplotlib
import random
from supplyclasses import *
import warnings
class TokenEconomy_Basic(TokenEconomy):
"""
Basic token economy class. It is using the following assumptions
1) There can be multiple agent pools whose transactions are denominated in fiat. They are independent.
2) There can be multiple pools whose transactions are denominated in the token. If so, then the transaction value
in fiat is calculated taking into account the current price.
3) Agent pools always act independently.
4) The holding time is an average across the whole ecosystem.
5) There can be external inputs into the system in fiat. if such inputs exist, then they are added into the overall transaction volume
At each iteration the following happens:
1) Calculate any income in fiat
2) Calculate the income from token sales, using the current price levels, and convert them to fiat.
3) Calculate holding time and supply
4) Use the pricing function to get the new price
Properties:
The TokenEconomy class has many properties, including some hidden ones. It's suggested
that you use the function get_data() to get access to those properties.
transactions_value_in_fiat: the effective total transaction volume in the fiat currenc.
transactions_volume_in_tokens: the effective total volume of transactions. It's
the user's choice as to whether this is taken into account for the holding time calculation
or ignored. This takes place via the holding_time controller.
holding_time: the holding time used in the simulation.
effective_holding_time: The holding time value that we would get if we would use equation of exchange
with holding time as an unknown.
num_users: The current number of users.
"""
def __init__(self,
holding_time:Union[float,HoldingTimeController],supply:Union[float,SupplyController],initial_price:float,
fiat:str='$',token:str='token',price_function:PriceFunctionController=PriceFunction_EOE,
price_function_parameters:Dict={},supply_pools:List[SupplyController]=[],
unit_of_time:str='month',agent_pools:List[AgentPool]=None)->None:
"""
Parameters
----------
holding_time : Union[float,HoldingTimeController]
DA controller determining how the holding time is being calculated..
supply : Union[float,SupplyController]
the core supply, or a supply controller that determines the release schedule.
initial_price : float
The starting price.
fiat : str, optional
The fiat currency. The default is '$'.
token : str, optional
The name of the token. The default is 'token'. This is also use in some checks, e.g. agent pools
must either be in fiat or the token.
price_function : PriceFunctionController, optional
A controller that determines the price of the token. The default is PriceFunction_EOE.
price_function_parameters : Dict, optional
the parameters for the pricing controller. The default is {}.
supply_pools : List[SupplyController], optional
Additional pools that might add or subtract from the supply, e.g. investors that are dumping tokens as they are vested
or a random burning mechanism. The default is [].
unit_of_time : str, optional
The unit of time The default is 'month'. This doesn't have any real effect on the simulations for now.
agent_pools : List[AgentPool], optional
The list of agent pools. The default is None.
Returns
-------
None
"""
super(TokenEconomy_Basic,self).__init__(holding_time=holding_time,supply=supply,fiat=fiat,token=token,
price_function=price_function,
unit_of_time=unit_of_time,token_initial_price=[initial_price])
if type(holding_time)==float or type(holding_time)==int:
self._holding_time_controller=HoldingTime_Constant(holding_time)
else:
self._holding_time_controller=holding_time
self._holding_time_controller.link(TokenEconomy,self)
if type(supply)==float or type(supply)==int:
self._supply=SupplyController_Constant(supply)
else:
self._supply=supply
self._supply.link(TokenEconomy,self)
self.price=initial_price
self._price_function=price_function(**price_function_parameters)
self._price_function.link(TokenEconomy,self)
#Note: The code is creating a list here, so that the structure is compatible with the structure of inherited classes
#which allow for multiple tokens
self.tokens=[token]
self.holding_time=self._holding_time_controller.get_holding_time()
if agent_pools!=None:
self.add_agent_pools(agent_pools)
if supply_pools!=None:
self.add_supply_pools(supply_pools)
self.initialised=False
return None
def add_agent_pool(self,agent_pool:AgentPool)->bool:
"""
Parameters
----------
agent_pool : AgentPool
The agent pool to be added. Appropriate tests are done.
Raises
------
Exception
Raises an exception if the pool does not have the appropriate token name or is not using fiat (either/or).
Returns
-------
bool
True if the pool has been added successfully.
"""
if agent_pool.currency!=self.fiat:
if np.logical_not(agent_pool.currency in self.tokens):
raise Exception('The currency of this agent pool is neither the fiat, nor the token')
if agent_pool.currency==self.fiat:
self.fiat_exists=True
if agent_pool.currency in self.tokens:
self.supply_exists=True
agent_pool.link(TokenEconomy,self)
self._agent_pools.append(agent_pool)
return True
def add_agent_pools(self,agent_pools_list:List)->bool:
"""
Convenience function for adding multiple agent pools in one go.
Parameters
----------
agent_pools_list : List
Returns
-------
bool
True if the pools were successfuly adeded.
"""
for ag in agent_pools_list:
self.add_agent_pool(ag)
return True
def add_supply_pool(self,supply_pool:SupplyController)->bool:
"""
Adds a supply pool. By default, it's assumed that the supply pool is supplying the token of the
token economy.
"""
supply_pool.link(TokenEconomy,self)
self._supply_pools.append(supply_pool)
return True
def add_supply_pools(self,supply_pools_list:List)->bool:
"""
Convenience function for adding multiple supply pools in one go.
Parameters
----------
supply_pools_list : List
Returns
-------
bool
True if the pools were successfuly added.
"""
for sup in supply_pools_list:
self.add_supply_pool(sup)
return True
def test_integrity(self)->bool:
"""
Tests whether the agent pools are compatible and whether the token economy
has the right parameters
"""
if len(self._agent_pools)==0:
raise Exception('You must define and link agent pools!')
for agent in self._agent_pools:
res=agent.test_integrity()
if not res:
raise Warning('Integrity of agent pool is not True. Name of pool: '+str(agent.name))
return False
if len(set(self._agent_pools))<len(self._agent_pools):
raise Warning('duplicate agent pools detected! It is likely the simulation will not run successfuly!')
return True
def initialise(self)->None:
"""
Initialises any agent pools that need to be initialised. These are pools that inherit from the Initialisable
class, and require initialisation in order to sort out their dependencies.
"""
for agent in self._agent_pools:
if isinstance(agent,Initialisable):
agent.initialise()
self.initialised=True
def execute(self)->bool:
"""
The main function which is used to simulate one round of the token economy.
The order of operations is as follows:
1. Initialise and test integrity.
2. Run the core supply controller.
3. Run the rest of the supply pools. Supply pools add to the total supply. This means
that if a pool returns a negative value, then this can have the effect of reducing the total supply
4. Run the holding time controller.
5. Execute all agent pools. Calculate an effective transaction volume in fiat and tokens at each iteration.
Agent pools can also exevute with negative transaction volumes, lowering the price.
6. Get the new number of users that each agent pool produces. Each agent pool can add users
to the total.
7. Calculate the new price and the new holding time
8. Store all the values generated in relevant store variables.
Return True if executed successfully.
"""
#First, we need to initialise any pools that have not been initialised, and then
#test the integrity of the token economy
if not self.initialised:
self.initialise()
if not self.test_integrity():
raise Exception('Integrity of pools not correct. Please make sure all agent pools have the correct dependencies.')
#These are the parameters which will be updated after this run of execute()
self.transactions_value_in_fiat=0
self.transactions_volume_in_tokens=0
#Run the core supply
self._supply.execute()
self.supply=self._supply.get_supply()
for supplypool in self._supply_pools:
supplypool.execute()
self.supply+=supplypool.get_supply()
if self.supply<=0:
warnings.warn('Warning! Supply reached 0! Iteration number :'+str(self.iteration))
return False
#Get the holding time
self.holding_time=self._holding_time_controller.get_holding_time()
#Execute agent pools
for agent in self._agent_pools:
agent.execute()
if agent.currency==self.token:
self.transactions_volume_in_tokens+=agent.get_transactions()
self.transactions_value_in_fiat+=agent.get_transactions()*self.price
elif agent.currency==self.fiat:
self.transactions_value_in_fiat+=agent.get_transactions()
if self.price>0:
self.transactions_volume_in_tokens=agent.get_transactions()*self.holding_time/self.price
else:
warnings.warn('Warning! Price reached 0 at iteration : '+str(self.iteration+1))
return False
else:
raise Exception('Agent pool found that does not function in neither fiat nor the token! Please specify correct currency!')
self.num_users+=agent.get_num_users()
#Calculate price and the new holding time
self._price_function.execute()
self._holding_time_controller.execute()
#Store the parameters
self.price=self._price_function.get_price()
self._holding_time_store.append(self.holding_time)
self._num_users_store.append(self.num_users)
self._supply_store.append(self.supply)
#this is the holding time if we were simply feeding back the equation of exchange on the current
#transaction volume in fiat and tokens
#We add a small number to prevent division by 0
self._effective_holding_time=self.price*self.supply/(self.transactions_value_in_fiat+0.000000001)
self._transactions_value_store_in_fiat.append(self.transactions_value_in_fiat)
self._prices_store.append(self.price)
self._transactions_value_store_in_tokens.append(self.transactions_volume_in_tokens)
self.iteration+=1
return True
def reset(self)->bool:
"""
Resets all agent pools and sets the iteration counter to 0.
"""
for agent in self._agent_pools:
agent.reset()
self.iteration=0
return True
def get_data(self)->pd.DataFrame:
"""
Returns a data frame with all the important variables for each iteration.
"""
df=pd.DataFrame({self.token+'_price':self._prices_store,'transactions_'+self.fiat:self._transactions_value_store_in_fiat,
'num_users':self._num_users_store,'iteration':np.arange(1,self.iteration+1),'holding_time':self._holding_time_store,
'effective_holding_time':self._effective_holding_time,'supply':self._supply_store})
df['transactions_'+self.token]=self._transactions_value_store_in_tokens
return df
class TokenMetaSimulator():
"""
This is a class that can be used to perform multiple simulations with a TokenEconomy class
and then returns the results.
"""
def __init__(self,token_economy:TokenEconomy)->None:
self.token_economy=copy.deepcopy(token_economy)
self.data=[]
self.repetitions=None
self.unit_of_time=token_economy.unit_of_time
def execute(self,iterations:int=36,repetitions=30)->pd.DataFrame:
iteration_runs=[]
repetition_reports=[]
for i in tqdm(range(repetitions)):
scipy.stats.rv_continuous.random_state==int(time.time()+int(np.random.rand()))
token_economy_copy=copy.deepcopy(self.token_economy)
token_economy_copy.reset()
it_current_data=[]
for j in range(iterations):
result=token_economy_copy.execute()
#it_current_data.append(token_economy_copy.get_state())
if not result:
break
it_current_data=token_economy_copy.get_data()
iteration_runs.append(it_current_data.copy())
it_current_data['repetition_run']=i
it_current_data['iteration_time']=np.arange(it_current_data.shape[0])
repetition_reports.append(it_current_data.copy())
self.data=pd.concat(repetition_reports)
self.repetitions=repetitions
return self.data
def get_data(self):
"""
Returns the simulated data
"""
return self.data
def get_report(self,functions:List=[np.mean,np.std,np.max,np.min],segment:Union[List,Tuple,int,str]=None)->pd.DataFrame:
"""
segment:
a)Either a list with indices [index1,index2] where index1<index2<repetitions
b) an integer, in which case we choose only a particular repetition
c) the keyword 'last'
"""
#
if segment==None:
averages=self.data.groupby('repetition_run').mean()
elif isinstance(segment,list) or isinstance(segment,tuple):
averages=self.data.loc[(self.data['iteration_time']>segment[0]) & (self.data['iteration_time']<segment[1]),:]
averages=averages.groupby('repetition_run').mean()
elif segment=='last':
averages=self.data[self.data['repetition_run']==self.repetitions].groupby('repetition_run').mean()
df_list=[]
for func in functions:
dummy=averages.apply(func)
dummy.name=func.__name__
df_list.append(dummy)
analysis_df=pd.concat(df_list,axis=1)
return analysis_df
def get_price_sensitivity_analysis(self):
if self.data==None:
raise Exception('You must first run execute() to collect data before using this function!')
pass
def get_timeseries(self,feature:str)->Union[matplotlib.collections.PolyCollection,pd.DataFrame]:
"""
Calculates an average for a certain feature in the report, (e.g. price) and then computes
a lineplot over time, plus 95% confidence interval. Returns both the plot and the transformed data.
"""
timeseries_mean=self.data.groupby('iteration_time')[feature].mean()
timeseries_std=self.data.groupby('iteration_time')[feature].std()
final=pd.DataFrame({feature+'_mean':timeseries_mean.values,
'sd':timeseries_std.values,
self.unit_of_time:np.arange(len(timeseries_mean))})
lower=timeseries_mean-1.96*timeseries_std
higher=timeseries_mean+1.96*timeseries_std
plt.plot(final[self.unit_of_time].values,timeseries_mean)
plt.xlabel(self.unit_of_time)
plt.ylabel(feature)
ax=plt.fill_between(final[self.unit_of_time],lower,higher,alpha=0.2)
plt.show()
return ax,final
Classes
class TokenEconomy_Basic (holding_time: Union[float, pricingclasses.HoldingTimeController], supply: Union[float, supplyclasses.SupplyController], initial_price: float, fiat: str = '$', token: str = 'token', price_function: pricingclasses.PriceFunctionController = pricingclasses.PriceFunction_EOE, price_function_parameters: Dict = {}, supply_pools: List[supplyclasses.SupplyController] = [], unit_of_time: str = 'month', agent_pools: List[baseclasses.AgentPool] = None)
-
Basic token economy class. It is using the following assumptions
1) There can be multiple agent pools whose transactions are denominated in fiat. They are independent. 2) There can be multiple pools whose transactions are denominated in the token. If so, then the transaction value
in fiat is calculated taking into account the current price. 3) Agent pools always act independently. 4) The holding time is an average across the whole ecosystem.
5) There can be external inputs into the system in fiat. if such inputs exist, then they are added into the overall transaction volumeAt each iteration the following happens:
1) Calculate any income in fiat 2) Calculate the income from token sales, using the current price levels, and convert them to fiat. 3) Calculate holding time and supply 4) Use the pricing function to get the new price
Properties
The TokenEconomy class has many properties, including some hidden ones. It's suggested that you use the function get_data() to get access to those properties.
transactions_value_in_fiat: the effective total transaction volume in the fiat currenc.
transactions_volume_in_tokens: the effective total volume of transactions. It's the user's choice as to whether this is taken into account for the holding time calculation or ignored. This takes place via the holding_time controller.
holding_time: the holding time used in the simulation.
effective_holding_time: The holding time value that we would get if we would use equation of exchange with holding time as an unknown.
num_users: The current number of users.
Parameters
holding_time
:Union[float,HoldingTimeController]
- DA controller determining how the holding time is being calculated..
supply
:Union[float,SupplyController]
- the core supply, or a supply controller that determines the release schedule.
initial_price
:float
- The starting price.
fiat
:str
, optional- The fiat currency. The default is '$'.
token
:str
, optional- The name of the token. The default is 'token'. This is also use in some checks, e.g. agent pools must either be in fiat or the token.
price_function
:PriceFunctionController
, optional- A controller that determines the price of the token. The default is PriceFunction_EOE.
price_function_parameters
:Dict
, optional- the parameters for the pricing controller. The default is {}.
supply_pools
:List[SupplyController]
, optional- Additional pools that might add or subtract from the supply, e.g. investors that are dumping tokens as they are vested or a random burning mechanism. The default is [].
unit_of_time
:str
, optional- The unit of time The default is 'month'. This doesn't have any real effect on the simulations for now.
agent_pools
:List[AgentPool]
, optional- The list of agent pools. The default is None.
Returns
None
Expand source code
class TokenEconomy_Basic(TokenEconomy): """ Basic token economy class. It is using the following assumptions 1) There can be multiple agent pools whose transactions are denominated in fiat. They are independent. 2) There can be multiple pools whose transactions are denominated in the token. If so, then the transaction value in fiat is calculated taking into account the current price. 3) Agent pools always act independently. 4) The holding time is an average across the whole ecosystem. 5) There can be external inputs into the system in fiat. if such inputs exist, then they are added into the overall transaction volume At each iteration the following happens: 1) Calculate any income in fiat 2) Calculate the income from token sales, using the current price levels, and convert them to fiat. 3) Calculate holding time and supply 4) Use the pricing function to get the new price Properties: The TokenEconomy class has many properties, including some hidden ones. It's suggested that you use the function get_data() to get access to those properties. transactions_value_in_fiat: the effective total transaction volume in the fiat currenc. transactions_volume_in_tokens: the effective total volume of transactions. It's the user's choice as to whether this is taken into account for the holding time calculation or ignored. This takes place via the holding_time controller. holding_time: the holding time used in the simulation. effective_holding_time: The holding time value that we would get if we would use equation of exchange with holding time as an unknown. num_users: The current number of users. """ def __init__(self, holding_time:Union[float,HoldingTimeController],supply:Union[float,SupplyController],initial_price:float, fiat:str='$',token:str='token',price_function:PriceFunctionController=PriceFunction_EOE, price_function_parameters:Dict={},supply_pools:List[SupplyController]=[], unit_of_time:str='month',agent_pools:List[AgentPool]=None)->None: """ Parameters ---------- holding_time : Union[float,HoldingTimeController] DA controller determining how the holding time is being calculated.. supply : Union[float,SupplyController] the core supply, or a supply controller that determines the release schedule. initial_price : float The starting price. fiat : str, optional The fiat currency. The default is '$'. token : str, optional The name of the token. The default is 'token'. This is also use in some checks, e.g. agent pools must either be in fiat or the token. price_function : PriceFunctionController, optional A controller that determines the price of the token. The default is PriceFunction_EOE. price_function_parameters : Dict, optional the parameters for the pricing controller. The default is {}. supply_pools : List[SupplyController], optional Additional pools that might add or subtract from the supply, e.g. investors that are dumping tokens as they are vested or a random burning mechanism. The default is []. unit_of_time : str, optional The unit of time The default is 'month'. This doesn't have any real effect on the simulations for now. agent_pools : List[AgentPool], optional The list of agent pools. The default is None. Returns ------- None """ super(TokenEconomy_Basic,self).__init__(holding_time=holding_time,supply=supply,fiat=fiat,token=token, price_function=price_function, unit_of_time=unit_of_time,token_initial_price=[initial_price]) if type(holding_time)==float or type(holding_time)==int: self._holding_time_controller=HoldingTime_Constant(holding_time) else: self._holding_time_controller=holding_time self._holding_time_controller.link(TokenEconomy,self) if type(supply)==float or type(supply)==int: self._supply=SupplyController_Constant(supply) else: self._supply=supply self._supply.link(TokenEconomy,self) self.price=initial_price self._price_function=price_function(**price_function_parameters) self._price_function.link(TokenEconomy,self) #Note: The code is creating a list here, so that the structure is compatible with the structure of inherited classes #which allow for multiple tokens self.tokens=[token] self.holding_time=self._holding_time_controller.get_holding_time() if agent_pools!=None: self.add_agent_pools(agent_pools) if supply_pools!=None: self.add_supply_pools(supply_pools) self.initialised=False return None def add_agent_pool(self,agent_pool:AgentPool)->bool: """ Parameters ---------- agent_pool : AgentPool The agent pool to be added. Appropriate tests are done. Raises ------ Exception Raises an exception if the pool does not have the appropriate token name or is not using fiat (either/or). Returns ------- bool True if the pool has been added successfully. """ if agent_pool.currency!=self.fiat: if np.logical_not(agent_pool.currency in self.tokens): raise Exception('The currency of this agent pool is neither the fiat, nor the token') if agent_pool.currency==self.fiat: self.fiat_exists=True if agent_pool.currency in self.tokens: self.supply_exists=True agent_pool.link(TokenEconomy,self) self._agent_pools.append(agent_pool) return True def add_agent_pools(self,agent_pools_list:List)->bool: """ Convenience function for adding multiple agent pools in one go. Parameters ---------- agent_pools_list : List Returns ------- bool True if the pools were successfuly adeded. """ for ag in agent_pools_list: self.add_agent_pool(ag) return True def add_supply_pool(self,supply_pool:SupplyController)->bool: """ Adds a supply pool. By default, it's assumed that the supply pool is supplying the token of the token economy. """ supply_pool.link(TokenEconomy,self) self._supply_pools.append(supply_pool) return True def add_supply_pools(self,supply_pools_list:List)->bool: """ Convenience function for adding multiple supply pools in one go. Parameters ---------- supply_pools_list : List Returns ------- bool True if the pools were successfuly added. """ for sup in supply_pools_list: self.add_supply_pool(sup) return True def test_integrity(self)->bool: """ Tests whether the agent pools are compatible and whether the token economy has the right parameters """ if len(self._agent_pools)==0: raise Exception('You must define and link agent pools!') for agent in self._agent_pools: res=agent.test_integrity() if not res: raise Warning('Integrity of agent pool is not True. Name of pool: '+str(agent.name)) return False if len(set(self._agent_pools))<len(self._agent_pools): raise Warning('duplicate agent pools detected! It is likely the simulation will not run successfuly!') return True def initialise(self)->None: """ Initialises any agent pools that need to be initialised. These are pools that inherit from the Initialisable class, and require initialisation in order to sort out their dependencies. """ for agent in self._agent_pools: if isinstance(agent,Initialisable): agent.initialise() self.initialised=True def execute(self)->bool: """ The main function which is used to simulate one round of the token economy. The order of operations is as follows: 1. Initialise and test integrity. 2. Run the core supply controller. 3. Run the rest of the supply pools. Supply pools add to the total supply. This means that if a pool returns a negative value, then this can have the effect of reducing the total supply 4. Run the holding time controller. 5. Execute all agent pools. Calculate an effective transaction volume in fiat and tokens at each iteration. Agent pools can also exevute with negative transaction volumes, lowering the price. 6. Get the new number of users that each agent pool produces. Each agent pool can add users to the total. 7. Calculate the new price and the new holding time 8. Store all the values generated in relevant store variables. Return True if executed successfully. """ #First, we need to initialise any pools that have not been initialised, and then #test the integrity of the token economy if not self.initialised: self.initialise() if not self.test_integrity(): raise Exception('Integrity of pools not correct. Please make sure all agent pools have the correct dependencies.') #These are the parameters which will be updated after this run of execute() self.transactions_value_in_fiat=0 self.transactions_volume_in_tokens=0 #Run the core supply self._supply.execute() self.supply=self._supply.get_supply() for supplypool in self._supply_pools: supplypool.execute() self.supply+=supplypool.get_supply() if self.supply<=0: warnings.warn('Warning! Supply reached 0! Iteration number :'+str(self.iteration)) return False #Get the holding time self.holding_time=self._holding_time_controller.get_holding_time() #Execute agent pools for agent in self._agent_pools: agent.execute() if agent.currency==self.token: self.transactions_volume_in_tokens+=agent.get_transactions() self.transactions_value_in_fiat+=agent.get_transactions()*self.price elif agent.currency==self.fiat: self.transactions_value_in_fiat+=agent.get_transactions() if self.price>0: self.transactions_volume_in_tokens=agent.get_transactions()*self.holding_time/self.price else: warnings.warn('Warning! Price reached 0 at iteration : '+str(self.iteration+1)) return False else: raise Exception('Agent pool found that does not function in neither fiat nor the token! Please specify correct currency!') self.num_users+=agent.get_num_users() #Calculate price and the new holding time self._price_function.execute() self._holding_time_controller.execute() #Store the parameters self.price=self._price_function.get_price() self._holding_time_store.append(self.holding_time) self._num_users_store.append(self.num_users) self._supply_store.append(self.supply) #this is the holding time if we were simply feeding back the equation of exchange on the current #transaction volume in fiat and tokens #We add a small number to prevent division by 0 self._effective_holding_time=self.price*self.supply/(self.transactions_value_in_fiat+0.000000001) self._transactions_value_store_in_fiat.append(self.transactions_value_in_fiat) self._prices_store.append(self.price) self._transactions_value_store_in_tokens.append(self.transactions_volume_in_tokens) self.iteration+=1 return True def reset(self)->bool: """ Resets all agent pools and sets the iteration counter to 0. """ for agent in self._agent_pools: agent.reset() self.iteration=0 return True def get_data(self)->pd.DataFrame: """ Returns a data frame with all the important variables for each iteration. """ df=pd.DataFrame({self.token+'_price':self._prices_store,'transactions_'+self.fiat:self._transactions_value_store_in_fiat, 'num_users':self._num_users_store,'iteration':np.arange(1,self.iteration+1),'holding_time':self._holding_time_store, 'effective_holding_time':self._effective_holding_time,'supply':self._supply_store}) df['transactions_'+self.token]=self._transactions_value_store_in_tokens return df
Ancestors
- baseclasses.TokenEconomy
Methods
def add_agent_pool(self, agent_pool: baseclasses.AgentPool) ‑> bool
-
Parameters
agent_pool
:AgentPool
- The agent pool to be added. Appropriate tests are done.
Raises
Exception
- Raises an exception if the pool does not have the appropriate token name or is not using fiat (either/or).
Returns
bool
- True if the pool has been added successfully.
Expand source code
def add_agent_pool(self,agent_pool:AgentPool)->bool: """ Parameters ---------- agent_pool : AgentPool The agent pool to be added. Appropriate tests are done. Raises ------ Exception Raises an exception if the pool does not have the appropriate token name or is not using fiat (either/or). Returns ------- bool True if the pool has been added successfully. """ if agent_pool.currency!=self.fiat: if np.logical_not(agent_pool.currency in self.tokens): raise Exception('The currency of this agent pool is neither the fiat, nor the token') if agent_pool.currency==self.fiat: self.fiat_exists=True if agent_pool.currency in self.tokens: self.supply_exists=True agent_pool.link(TokenEconomy,self) self._agent_pools.append(agent_pool) return True
def add_agent_pools(self, agent_pools_list: List) ‑> bool
-
Convenience function for adding multiple agent pools in one go.
Parameters
agent_pools_list
:List
Returns
bool
- True if the pools were successfuly adeded.
Expand source code
def add_agent_pools(self,agent_pools_list:List)->bool: """ Convenience function for adding multiple agent pools in one go. Parameters ---------- agent_pools_list : List Returns ------- bool True if the pools were successfuly adeded. """ for ag in agent_pools_list: self.add_agent_pool(ag) return True
def add_supply_pool(self, supply_pool: supplyclasses.SupplyController) ‑> bool
-
Adds a supply pool. By default, it's assumed that the supply pool is supplying the token of the token economy.
Expand source code
def add_supply_pool(self,supply_pool:SupplyController)->bool: """ Adds a supply pool. By default, it's assumed that the supply pool is supplying the token of the token economy. """ supply_pool.link(TokenEconomy,self) self._supply_pools.append(supply_pool) return True
def add_supply_pools(self, supply_pools_list: List) ‑> bool
-
Convenience function for adding multiple supply pools in one go.
Parameters
supply_pools_list
:List
Returns
bool
- True if the pools were successfuly added.
Expand source code
def add_supply_pools(self,supply_pools_list:List)->bool: """ Convenience function for adding multiple supply pools in one go. Parameters ---------- supply_pools_list : List Returns ------- bool True if the pools were successfuly added. """ for sup in supply_pools_list: self.add_supply_pool(sup) return True
def execute(self) ‑> bool
-
The main function which is used to simulate one round of the token economy.
The order of operations is as follows:
-
Initialise and test integrity.
-
Run the core supply controller.
-
Run the rest of the supply pools. Supply pools add to the total supply. This means that if a pool returns a negative value, then this can have the effect of reducing the total supply
-
Run the holding time controller.
-
Execute all agent pools. Calculate an effective transaction volume in fiat and tokens at each iteration. Agent pools can also exevute with negative transaction volumes, lowering the price.
-
Get the new number of users that each agent pool produces. Each agent pool can add users to the total.
-
Calculate the new price and the new holding time
-
Store all the values generated in relevant store variables.
Return True if executed successfully.
Expand source code
def execute(self)->bool: """ The main function which is used to simulate one round of the token economy. The order of operations is as follows: 1. Initialise and test integrity. 2. Run the core supply controller. 3. Run the rest of the supply pools. Supply pools add to the total supply. This means that if a pool returns a negative value, then this can have the effect of reducing the total supply 4. Run the holding time controller. 5. Execute all agent pools. Calculate an effective transaction volume in fiat and tokens at each iteration. Agent pools can also exevute with negative transaction volumes, lowering the price. 6. Get the new number of users that each agent pool produces. Each agent pool can add users to the total. 7. Calculate the new price and the new holding time 8. Store all the values generated in relevant store variables. Return True if executed successfully. """ #First, we need to initialise any pools that have not been initialised, and then #test the integrity of the token economy if not self.initialised: self.initialise() if not self.test_integrity(): raise Exception('Integrity of pools not correct. Please make sure all agent pools have the correct dependencies.') #These are the parameters which will be updated after this run of execute() self.transactions_value_in_fiat=0 self.transactions_volume_in_tokens=0 #Run the core supply self._supply.execute() self.supply=self._supply.get_supply() for supplypool in self._supply_pools: supplypool.execute() self.supply+=supplypool.get_supply() if self.supply<=0: warnings.warn('Warning! Supply reached 0! Iteration number :'+str(self.iteration)) return False #Get the holding time self.holding_time=self._holding_time_controller.get_holding_time() #Execute agent pools for agent in self._agent_pools: agent.execute() if agent.currency==self.token: self.transactions_volume_in_tokens+=agent.get_transactions() self.transactions_value_in_fiat+=agent.get_transactions()*self.price elif agent.currency==self.fiat: self.transactions_value_in_fiat+=agent.get_transactions() if self.price>0: self.transactions_volume_in_tokens=agent.get_transactions()*self.holding_time/self.price else: warnings.warn('Warning! Price reached 0 at iteration : '+str(self.iteration+1)) return False else: raise Exception('Agent pool found that does not function in neither fiat nor the token! Please specify correct currency!') self.num_users+=agent.get_num_users() #Calculate price and the new holding time self._price_function.execute() self._holding_time_controller.execute() #Store the parameters self.price=self._price_function.get_price() self._holding_time_store.append(self.holding_time) self._num_users_store.append(self.num_users) self._supply_store.append(self.supply) #this is the holding time if we were simply feeding back the equation of exchange on the current #transaction volume in fiat and tokens #We add a small number to prevent division by 0 self._effective_holding_time=self.price*self.supply/(self.transactions_value_in_fiat+0.000000001) self._transactions_value_store_in_fiat.append(self.transactions_value_in_fiat) self._prices_store.append(self.price) self._transactions_value_store_in_tokens.append(self.transactions_volume_in_tokens) self.iteration+=1 return True
-
def get_data(self) ‑> pandas.core.frame.DataFrame
-
Returns a data frame with all the important variables for each iteration.
Expand source code
def get_data(self)->pd.DataFrame: """ Returns a data frame with all the important variables for each iteration. """ df=pd.DataFrame({self.token+'_price':self._prices_store,'transactions_'+self.fiat:self._transactions_value_store_in_fiat, 'num_users':self._num_users_store,'iteration':np.arange(1,self.iteration+1),'holding_time':self._holding_time_store, 'effective_holding_time':self._effective_holding_time,'supply':self._supply_store}) df['transactions_'+self.token]=self._transactions_value_store_in_tokens return df
def initialise(self) ‑> None
-
Initialises any agent pools that need to be initialised. These are pools that inherit from the Initialisable class, and require initialisation in order to sort out their dependencies.
Expand source code
def initialise(self)->None: """ Initialises any agent pools that need to be initialised. These are pools that inherit from the Initialisable class, and require initialisation in order to sort out their dependencies. """ for agent in self._agent_pools: if isinstance(agent,Initialisable): agent.initialise() self.initialised=True
def reset(self) ‑> bool
-
Resets all agent pools and sets the iteration counter to 0.
Expand source code
def reset(self)->bool: """ Resets all agent pools and sets the iteration counter to 0. """ for agent in self._agent_pools: agent.reset() self.iteration=0 return True
def test_integrity(self) ‑> bool
-
Tests whether the agent pools are compatible and whether the token economy has the right parameters
Expand source code
def test_integrity(self)->bool: """ Tests whether the agent pools are compatible and whether the token economy has the right parameters """ if len(self._agent_pools)==0: raise Exception('You must define and link agent pools!') for agent in self._agent_pools: res=agent.test_integrity() if not res: raise Warning('Integrity of agent pool is not True. Name of pool: '+str(agent.name)) return False if len(set(self._agent_pools))<len(self._agent_pools): raise Warning('duplicate agent pools detected! It is likely the simulation will not run successfuly!') return True
class TokenMetaSimulator (token_economy: baseclasses.TokenEconomy)
-
This is a class that can be used to perform multiple simulations with a TokenEconomy class and then returns the results.
Expand source code
class TokenMetaSimulator(): """ This is a class that can be used to perform multiple simulations with a TokenEconomy class and then returns the results. """ def __init__(self,token_economy:TokenEconomy)->None: self.token_economy=copy.deepcopy(token_economy) self.data=[] self.repetitions=None self.unit_of_time=token_economy.unit_of_time def execute(self,iterations:int=36,repetitions=30)->pd.DataFrame: iteration_runs=[] repetition_reports=[] for i in tqdm(range(repetitions)): scipy.stats.rv_continuous.random_state==int(time.time()+int(np.random.rand())) token_economy_copy=copy.deepcopy(self.token_economy) token_economy_copy.reset() it_current_data=[] for j in range(iterations): result=token_economy_copy.execute() #it_current_data.append(token_economy_copy.get_state()) if not result: break it_current_data=token_economy_copy.get_data() iteration_runs.append(it_current_data.copy()) it_current_data['repetition_run']=i it_current_data['iteration_time']=np.arange(it_current_data.shape[0]) repetition_reports.append(it_current_data.copy()) self.data=pd.concat(repetition_reports) self.repetitions=repetitions return self.data def get_data(self): """ Returns the simulated data """ return self.data def get_report(self,functions:List=[np.mean,np.std,np.max,np.min],segment:Union[List,Tuple,int,str]=None)->pd.DataFrame: """ segment: a)Either a list with indices [index1,index2] where index1<index2<repetitions b) an integer, in which case we choose only a particular repetition c) the keyword 'last' """ # if segment==None: averages=self.data.groupby('repetition_run').mean() elif isinstance(segment,list) or isinstance(segment,tuple): averages=self.data.loc[(self.data['iteration_time']>segment[0]) & (self.data['iteration_time']<segment[1]),:] averages=averages.groupby('repetition_run').mean() elif segment=='last': averages=self.data[self.data['repetition_run']==self.repetitions].groupby('repetition_run').mean() df_list=[] for func in functions: dummy=averages.apply(func) dummy.name=func.__name__ df_list.append(dummy) analysis_df=pd.concat(df_list,axis=1) return analysis_df def get_price_sensitivity_analysis(self): if self.data==None: raise Exception('You must first run execute() to collect data before using this function!') pass def get_timeseries(self,feature:str)->Union[matplotlib.collections.PolyCollection,pd.DataFrame]: """ Calculates an average for a certain feature in the report, (e.g. price) and then computes a lineplot over time, plus 95% confidence interval. Returns both the plot and the transformed data. """ timeseries_mean=self.data.groupby('iteration_time')[feature].mean() timeseries_std=self.data.groupby('iteration_time')[feature].std() final=pd.DataFrame({feature+'_mean':timeseries_mean.values, 'sd':timeseries_std.values, self.unit_of_time:np.arange(len(timeseries_mean))}) lower=timeseries_mean-1.96*timeseries_std higher=timeseries_mean+1.96*timeseries_std plt.plot(final[self.unit_of_time].values,timeseries_mean) plt.xlabel(self.unit_of_time) plt.ylabel(feature) ax=plt.fill_between(final[self.unit_of_time],lower,higher,alpha=0.2) plt.show() return ax,final
Methods
def execute(self, iterations: int = 36, repetitions=30) ‑> pandas.core.frame.DataFrame
-
Expand source code
def execute(self,iterations:int=36,repetitions=30)->pd.DataFrame: iteration_runs=[] repetition_reports=[] for i in tqdm(range(repetitions)): scipy.stats.rv_continuous.random_state==int(time.time()+int(np.random.rand())) token_economy_copy=copy.deepcopy(self.token_economy) token_economy_copy.reset() it_current_data=[] for j in range(iterations): result=token_economy_copy.execute() #it_current_data.append(token_economy_copy.get_state()) if not result: break it_current_data=token_economy_copy.get_data() iteration_runs.append(it_current_data.copy()) it_current_data['repetition_run']=i it_current_data['iteration_time']=np.arange(it_current_data.shape[0]) repetition_reports.append(it_current_data.copy()) self.data=pd.concat(repetition_reports) self.repetitions=repetitions return self.data
def get_data(self)
-
Returns the simulated data
Expand source code
def get_data(self): """ Returns the simulated data """ return self.data
def get_price_sensitivity_analysis(self)
-
Expand source code
def get_price_sensitivity_analysis(self): if self.data==None: raise Exception('You must first run execute() to collect data before using this function!') pass
def get_report(self, functions: List = [<function mean>, <function std>, <function amax>, <function amin>], segment: Union[List, Tuple, int, str] = None) ‑> pandas.core.frame.DataFrame
-
segment: a)Either a list with indices [index1,index2] where index1<index2<repetitions b) an integer, in which case we choose only a particular repetition c) the keyword 'last'
Expand source code
def get_report(self,functions:List=[np.mean,np.std,np.max,np.min],segment:Union[List,Tuple,int,str]=None)->pd.DataFrame: """ segment: a)Either a list with indices [index1,index2] where index1<index2<repetitions b) an integer, in which case we choose only a particular repetition c) the keyword 'last' """ # if segment==None: averages=self.data.groupby('repetition_run').mean() elif isinstance(segment,list) or isinstance(segment,tuple): averages=self.data.loc[(self.data['iteration_time']>segment[0]) & (self.data['iteration_time']<segment[1]),:] averages=averages.groupby('repetition_run').mean() elif segment=='last': averages=self.data[self.data['repetition_run']==self.repetitions].groupby('repetition_run').mean() df_list=[] for func in functions: dummy=averages.apply(func) dummy.name=func.__name__ df_list.append(dummy) analysis_df=pd.concat(df_list,axis=1) return analysis_df
def get_timeseries(self, feature: str) ‑> Union[matplotlib.collections.PolyCollection, pandas.core.frame.DataFrame]
-
Calculates an average for a certain feature in the report, (e.g. price) and then computes a lineplot over time, plus 95% confidence interval. Returns both the plot and the transformed data.
Expand source code
def get_timeseries(self,feature:str)->Union[matplotlib.collections.PolyCollection,pd.DataFrame]: """ Calculates an average for a certain feature in the report, (e.g. price) and then computes a lineplot over time, plus 95% confidence interval. Returns both the plot and the transformed data. """ timeseries_mean=self.data.groupby('iteration_time')[feature].mean() timeseries_std=self.data.groupby('iteration_time')[feature].std() final=pd.DataFrame({feature+'_mean':timeseries_mean.values, 'sd':timeseries_std.values, self.unit_of_time:np.arange(len(timeseries_mean))}) lower=timeseries_mean-1.96*timeseries_std higher=timeseries_mean+1.96*timeseries_std plt.plot(final[self.unit_of_time].values,timeseries_mean) plt.xlabel(self.unit_of_time) plt.ylabel(feature) ax=plt.fill_between(final[self.unit_of_time],lower,higher,alpha=0.2) plt.show() return ax,final