Modeling as a weighted directed graph with agents as nodes. A network is a set of items (nodes or vertices) connected by edges or links. We represent a network by a graph (N, g), which consists of a set of nodes N = {1, . . . , n}.
An agent is a user of the CIC system. In the case of our simulation, agents are subpopulation representation based off of kmeans clustering of the real system data.
A chama is a savings group consisting of multiple agents. Redemptions of CICs for fiat occur through chamas.
A trader is an agent interacting with the bonding curve for investment/arbitrage opportunities.
The cloud is a representation of the open boundary to the world external to the model.
The contract is the smart contract of the bonding curve.
The edge weight gij > 0 takes on non-binary values, representing the intensity of the interaction, so we refer to (N, g) as a weighted graph. E is the set of “directed” edges, i.e., (i, j) ∈ E
from IPython.display import display, Image
display(Image(filename='images/dualoperator.png'))
To create a representative model of the agent interactions, we will use subpopulation modeling. We take all of the agents and cluster them based off of the following features from full population actual transactional data from Jan - May 11 2020 xDai data (s means source, t means target):
Essentially, we are taking a graph zoom operation, bundling nodes together based off of their likeness. Nodes are constant with edges being transative. The algorithm we use for this graph zoom operation is Kmeans clustering. Based off our descriptive statistical analysis and use of th Gap Statistic created by Stanford researchers Tibshirani, Walther and Hastie in their 2001 paper, we determined 50 clusters are representative of the subpopulations. All of the flows inside of the bundle become part of the self-loop flow. For example, within cluster 1, agent a can transaction with as b. This will not be reflected within our model as this is intra not inter cluster interactions.
display(Image(filename='images/gap_statistic.png'))
display(Image(filename='images/pca.png'))
Chamas are currently set to zero, it can be configured for more detailed analysis later on.
Traders are currently set to zero, it can be configured for more detailed analysis later on.
Utility types ordered and probability are calculated per cluster off of the real transactional data. Below is types and probability for cluster 2:
Bonding curves are continuous liquidity mechanisms which are used in market design for cryptographically-supported token economies. Bonding curves are an example of an enforceable mechanism through which participating agents influence this state.
∗ The spot price is the limiting price for both the Bond-to-Mint and the Burn-to-Withdraw Mechanisms in the case with no fees. Realized prices account for slippage and fees, see references.
† Bonding Curves such as the one being employed by the CIC systems are tools which enforce the Reserve Ratio to be a constant ρ∈(0,1), also called the connector weight. In this work we work with the "curvature" κ=1ρ=P⋅SR in order to align notation with associated academic work.
If the controller wants to burn, the amount decided from the inventory controller, ΔS is inserted into the following minting equation:
display(Image(filename='images/v4differentialspec.png'))
Every 90 days, the Red Cross drips 10,000 shilling to the grassroots operator fiat balance.
Agent generation for each time step: Random choice of all agents minus 5 for both paying and receiving.
Agent demand each timestep: demand distributions based off of gaussian for each utility type.
if vi,jdi,j∗1−ϕ>γiandvi,jdi,j∗ϕ>γcic⇒ζ=vi,jdi,j
else ⇒ζ=γ
Allocate utility type by stack ranking in. Allocate remaining fiat and cic until all demand is met or i runs out.
The user is able to withdraw up to 50% of the their CIC balance if they have spent 50% of their balance within the last 30 days at a conversion ratio of 1:1, meaning that for every one token withdraw, they receive 1 in native currency. We are assuming that agents want what to withdraw as much as they can. This is one of the most important control points for Grassroots economics. The more people withdraw CIC from the system, the more difficult it is on the system. The more people can withdraw, the better the adoption however. The inverse also holds true: the less individuals can withdraw, the lower the adoption. 30,000 is the max allowable amount to be withdraw per 30 days. Agents are charged a 5% withdraw fee when converting their CICs to shillings.
# agent:[centrality,allocationValue]
agentAllocation = {'0':[1,1],'1':[1,1],etc}
Every 30 days, a total of unadjustedPerAgent * agents will be distributed among the agents. Allocation will occur based off of the the agent allocation dictionary allocation value. We can optimize the allocation overtime and make a state variable for adjustment overtime as a result of centrality. We are currently assuming that all agents have the same centrality and allocation.
Internal velocity is better than external velocity of the system. Point of leverage to make more internal cycles. Can be used for tuning system effiency.
display(Image(filename='images/agentDistribution.png'))
Heuristic Monetary policy hysteresis conservation allocation between fiat and cic reserves. We've created an inventory control function to test if the current balance is in an acceptable tolarance. For the calculation, we use the following 2 variables, current CIC balance and current fiat balance, along with 2 parameters, desired cic and variance.
We have a parameter called inventory_controller which uses a boolean operator. Currently, the inventory controller is turned off with the value of False.
Below is
if idealFiat - varianceFiat <= actualFiat <= idealFiat + (2*varianceFiat):
decision = 'none'
amount = 0
else:
if (idealFiat - varianceFiat) > actualFiat:
decision = 'burn'
amount = (idealFiat + varianceFiat) - actualFiat
else:
pass
if actualFiat > (idealFiat + varianceFiat):
decision = 'mint'
amount = actualFiat - (idealFiat + varianceFiat)
else:
pass
if decision == 'mint':
if actualCIC < (idealCIC - varianceCIC):
if amount > actualCIC:
decision = 'none'
amount = 0
else:
pass
if decision == 'none':
if actualCIC < (idealCIC - varianceCIC):
decision = 'mint'
amount = (idealCIC-varianceCIC)
else:
pass
If the controller wants to mint, the amount decided from the inventory controller, ΔR is inserted into the following minting equation (as described above in the bonding curve section):
If the controller wants to burn, the amount decided from the inventory controller, ΔS is inserted into the following minting equation:
There is a built in process lag of 7 days before the newly minted or burned CIC is added to the respective operator accounts.
Indirect measurement of velocity of money per timestep:
Vt=PTM
Where
display(Image(filename='images/experiments.png'))
Update subpopulations by geography. Meta population model.
Weighted edges for choosing probability of agenters interacting. I.e. subpopulations interact with the same 5 subpopulations primarily.
Provide an ability to derive the fiscal multiplier of red cross drips.
Payer and receiver needs to have separate amount of cic demanded. Payer wants to pay in 100% fiat whereas receiver may only want 5% fiat. Need to reconcile the two and track the difference.
Advanced algorithic inventory allocation
Percentage of k cycles centrality- for rewards feedback/basic income
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from model.parts.supportingFunctions import *
pd.options.display.float_format = '{:.2f}'.format
%matplotlib inline
# The following imports NEED to be in the exact order
from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
from cadCAD import configs
from model import economyconfig
exec_mode = ExecutionMode()
local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
simulation = Executor(exec_context=local_mode_ctx, configs=configs)
raw_system_events, tensor_field, sessions = simulation.execute()
df = pd.DataFrame(raw_system_events)
___________ ____ ________ __ ___/ / ____/ | / __ \ / ___/ __` / __ / / / /| | / / / / / /__/ /_/ / /_/ / /___/ ___ |/ /_/ / \___/\__,_/\__,_/\____/_/ |_/_____/ by cadCAD Execution Mode: local_proc Configuration Count: 1 Dimensions of the first simulation: (Timesteps, Params, Runs, Vars) = (100, 1, 5, 18) Execution Method: local_simulations SimIDs : [0, 0, 0, 0, 0] SubsetIDs: [0, 0, 0, 0, 0] Ns : [0, 1, 2, 3, 4] ExpIDs : [0, 0, 0, 0, 0] Execution Mode: parallelized
/home/aclarkdata/anaconda3/lib/python3.8/site-packages/cadCAD/utils/__init__.py:124: FutureWarning: The use of a dictionary to describe Partial State Update Blocks will be deprecated. Use a list instead. warnings.warn(
Total execution time: 115.53s
df.head()
network | KPIDemand | KPISpend | KPISpendOverDemand | VelocityOfMoney | startingBalance | 30_day_spend | withdraw | outboundAgents | inboundAgents | ... | totalDistributedToAgents | totalMinted | totalBurned | exitFeeRevenue | drip | simulation | subset | run | substep | timestep | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | {} | {} | {} | 0.00 | {} | {} | {} | [] | [] | ... | 0 | 0 | 0 | 0.00 | 10000 | 0 | 0 | 1 | 0 | 0 |
1 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | {} | {} | {} | 0.00 | {'0': 150.0, '1': 340.0, '2': 250.0, '3': 20.0... | {} | {} | [] | [] | ... | 0 | 0 | 0 | 0.00 | 10000 | 0 | 0 | 1 | 1 | 1 |
2 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | {} | {} | {} | 0.00 | {'0': 150.0, '1': 340.0, '2': 250.0, '3': 20.0... | {} | {} | [] | [] | ... | 0 | 0 | 0 | 0.00 | 10000 | 0 | 0 | 1 | 2 | 1 |
3 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | {} | {} | {} | 0.00 | {'0': 150.0, '1': 340.0, '2': 250.0, '3': 20.0... | {} | {} | [8, 24, 13, 6, 19, 39, 42, 34, 7, 34, 24, 20, ... | [34, 36, 25, 8, 29, 0, 29, 47, 49, 40, 38, 14,... | ... | 0 | 0 | 0 | 0.00 | 10000 | 0 | 0 | 1 | 3 | 1 |
4 | (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... | {} | {} | {} | 0.00 | {'0': 150.0, '1': 340.0, '2': 250.0, '3': 20.0... | {} | {} | [8, 24, 13, 6, 19, 39, 42, 34, 7, 34, 24, 20, ... | [34, 36, 25, 8, 29, 0, 29, 47, 49, 40, 38, 14,... | ... | 0 | 0 | 0 | 0.00 | 10000 | 0 | 0 | 1 | 4 | 1 |
5 rows × 23 columns
df.substep.unique()
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
df.columns
Index(['network', 'KPIDemand', 'KPISpend', 'KPISpendOverDemand', 'VelocityOfMoney', 'startingBalance', '30_day_spend', 'withdraw', 'outboundAgents', 'inboundAgents', 'operatorFiatBalance', 'operatorCICBalance', 'fundsInProcess', 'totalDistributedToAgents', 'totalMinted', 'totalBurned', 'exitFeeRevenue', 'drip', 'simulation', 'subset', 'run', 'substep', 'timestep'], dtype='object')
df.run.unique()
array([1, 2, 3, 4, 5])
# extract agent names from networkx object and create new column
df['agents'] = df.network.apply(lambda g: np.array([get_nodes_by_type(g,'Agent')][0]))
# extract agent tokens from networkx object and create new column
df['agent_tokens'] = df.network.apply(lambda g: np.array([g.nodes[j]['tokens'] for j in get_nodes_by_type(g,'Agent')]))
# extract agent currency from networkx object and create new column
df['agent_native_currency'] = df.network.apply(lambda g: np.array([g.nodes[j]['native_currency'] for j in get_nodes_by_type(g,'Agent')]))
# Create dataframe variables to aggregate dictionary values
tokens = []
for i in df.index:
tokens.append(sum(df['agent_tokens'][i]))
df['AggregatedAgentCICHolding'] = tokens
withdraw = []
for i in df.index:
try:
withdraw.append(sum(df['withdraw'][i].values()))
except:
withdraw.append(0)
df['AggregatedWithdraw'] = withdraw
currency = []
for i in df.index:
currency.append(sum(df['agent_native_currency'][i]))
df['AggregatedAgentCurrencyHolding'] = currency
AggregatedSpend = []
for i in df.index:
AggregatedSpend.append(sum(df['KPISpend'][i].values()))
df['AggregatedAgentSpend'] = AggregatedSpend
AggregatedDemand = []
for i in df.index:
AggregatedDemand.append(sum(df['KPIDemand'][i].values()))
df['AggregatedAgentDemand'] = AggregatedDemand
AggregatedKPISpendOverDemand = []
for i in df.index:
AggregatedKPISpendOverDemand.append(sum(df['KPISpendOverDemand'][i].values()))
df['AggregatedKPISpendOverDemand'] = AggregatedKPISpendOverDemand
AggregatedGapOfDemandMinusSpend = []
for i in df.index:
AggregatedGapOfDemandMinusSpend.append(sum(df['KPIDemand'][i].values())- sum(df['KPISpend'][i].values()))
df['AggregatedGapOfDemandMinusSpend'] = AggregatedGapOfDemandMinusSpend
# subset dataframe for aggregation of runs
subset = df[['timestep', 'VelocityOfMoney', 'operatorFiatBalance',
'operatorCICBalance', 'totalDistributedToAgents', 'totalMinted',
'totalBurned', 'AggregatedWithdraw','run', 'drip', 'substep', 'exitFeeRevenue', 'AggregatedAgentCICHolding',
'AggregatedAgentCurrencyHolding', 'AggregatedAgentSpend',
'AggregatedAgentDemand', 'AggregatedKPISpendOverDemand',
'AggregatedGapOfDemandMinusSpend']]
# plot of agent spend per timestep
plot_median_with_quantiles_annotation(subset,'timestep','timestep','AggregatedAgentSpend')
plot_fan_chart(subset,'timestep','timestep','AggregatedAgentSpend')
# plot the velocity of money per timestep
plot_fan_chart(subset,'timestep','timestep','VelocityOfMoney')
# plot the operatorCIC balance using a fan chart
plot_fan_chart(subset,'timestep','timestep','operatorCICBalance')
/home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:384: RuntimeWarning: divide by zero encountered in double_scalars inv_avg_iqr = [1/i for i in avg_iqr] /home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:385: RuntimeWarning: invalid value encountered in double_scalars norm_avg_iqr = [i/max(inv_avg_iqr) for i in inv_avg_iqr]
plot_fan_chart(subset,'timestep','timestep','operatorFiatBalance')
/home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:384: RuntimeWarning: divide by zero encountered in double_scalars inv_avg_iqr = [1/i for i in avg_iqr] /home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:385: RuntimeWarning: invalid value encountered in double_scalars norm_avg_iqr = [i/max(inv_avg_iqr) for i in inv_avg_iqr]
plot_fan_chart(subset,'timestep','timestep','drip')
/home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:384: RuntimeWarning: divide by zero encountered in long_scalars inv_avg_iqr = [1/i for i in avg_iqr] /home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:385: RuntimeWarning: invalid value encountered in double_scalars norm_avg_iqr = [i/max(inv_avg_iqr) for i in inv_avg_iqr]
plot_fan_chart(subset,'timestep','timestep','exitFeeRevenue')
plot_median_with_quantiles(subset,'timestep','timestep','totalMinted')
plot_median_with_quantiles(subset,'timestep','timestep','totalBurned')
plot_fan_chart(subset,'timestep','timestep','AggregatedAgentCICHolding')
plot_fan_chart(subset,'timestep','timestep','AggregatedAgentCurrencyHolding')
plot_fan_chart(subset,'timestep','timestep','AggregatedAgentDemand')
plot_fan_chart(subset,'timestep','timestep','AggregatedWithdraw')
/home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:384: RuntimeWarning: divide by zero encountered in double_scalars inv_avg_iqr = [1/i for i in avg_iqr] /home/aclarkdata/repos/cic-modeling/Simulation/model/parts/supportingFunctions.py:385: RuntimeWarning: invalid value encountered in double_scalars norm_avg_iqr = [i/max(inv_avg_iqr) for i in inv_avg_iqr]