Simulation Inputs
How to configure a new simulation
Introduction
A high level description of the different parameters and how they effectively impact the simulation outcomes can be found here.
Each simulation experiment has its inputs and outputs stored in an experiment directory.
To run an experiment, we create the folder with 2 subfolders, inputs
and outputs
; fill the inputs subfolder with our set of assumptions and reference data; then call the simulation engine.
The configuration of an experiment covers the following aspects:
General manifest.
Price trajectories.
User Behavior.
Environment.
Concrete protocol.
Engine parameters.
Experiment manifest
The first piece of configuration that goes into the inputs folder is a simple manifest describing the experiment:
manifest = {
'id': string,
'date': string,
'mode': 'single or 'montecarlo',
'description': stirng
}
mode
: When using single mode, the engine will run one single scenario defined and provided by the user; in montecarlo mode, the engine will generate n price trajectories and run their multiple scenarios.
Price Trajectories
Price Trajectories for montecarlo mode
price_trajectories_params = {
'num_trajectories': 50,
'pattern_num_hours': 120,
'initial_prices': { 'btc': 30000, 'eth': 1800 },
'horizon': 365,
'bullishness': 0.5
}
num_trajectories
: Number of trajectories that will be generated and simulated.pattern_num_hours
: When generating random price lines, we shuffle historical price action by picking random chunks of N hours to conserve some patterns in price action. This number specifies the length of these chunks.initial_prices
: Prices of BTC and ETH at day 0.horizon
: Number of days simulated in each scenario.bullishness
: Parameter between 0 and 1 to select more trajectories with positive vs negative returns from start to end. Setting to 0.5 means we will simulate half trajectories with positive return from start to end.
In addition to these parameters, the following CSV files have to be provided as inputs:
history.csv
: A table containing the price feeds that will be used as a reference to draw random chunks from, and compose random price trajectories.slippage.csv
: A table containing the slippage statistics for BTC and ETH.interest_rates.csv
: Historical interest rates for a lending protocol to be joined with the reference data. So when we draw random chunks of price action, we also get the corresponding lending market interest rates.
Price Trajectory for single mode
In single scenario mode, one price trajectory has to be provided in the inputs folder as a CSV file.
trajectory.csv
: A table containing the price feeds, slippage statistics, and interest rates.
User Behavior
behavior_params = {
'supply_sizes': {
'S':...,
'M':...,
'L':...,
'W':...},
'ltvs': {
'STABLE':...,
'WBTC':...,
'WETH':...},
'buy_policy': [
{'prct': 100,
'delta_range': [0.00, 0.05],
'condition': "(current_ltv > (max_ltv - delta))"}
]
'default_rates': {
'S': [0.005, 0.022, 0.044, 0.188],
'M': [0.004, 0.024, 0.060, 0.210],
'L': [0.003, 0.014, 0.021, 0.124],
'W': [0.001, 0.001, 0.001, 0.062] },
'default_rates_stress_factor': 1.0,
'new_policy_bottom_ltv': 0.5,
'new_policy_bottom_proba': 0.1,
'new_policy_top_proba': 0.5,
'useclaim_proba': 0.75
}
This part defines a series of assumptions about user behaviors:
supply_sizes
: the distribution of supply size amount by category of user: S=Under $50k; M=50 to $250k; L=250 to $2M and W=2 to $10M.ltvs
: the distribution of LTVs for positions by collateral type.buy_policy
: this section defines a list of conditions under which users will buy protection policies. Each item here has:percentage: fraction of users who fall into this condition.
delta_range: the LTV delta tranche at which these users will buy policies.
condition: the condition at which they will buy policies, written as a python expression taking parameters such as current_ltv and delta (a number randomly picked for each agent from the delta_range defined above.)
default_rates
: assuming users are divided in 4 credit risk categories representing each 25% of the population; what are the default rates associated with each category.default_rates_stress_factor
: To facilitate making changes to the default_rates table, this stress factor simply multiplies all the numbers to increase or reduce them, rather than change them one by one.useclaim_proba
: The probability of using the protection when LTV_PROTECT is breached, rather than adding the collateral to avoid paying interests.
For more details on how these params are interpreted by the simulation engine, the code lives in the UserBehavior class.
In addition to these parameters, a profiles file needs to be provided, containing examples of borrow/supply percentages per token.
profiles.csv
: a list of different combinations of supply and borrow percentages across BTC, ETH and STABLE.
Environment
Population
population_params = {
'size': 1000,
'cohorts': {'S': 35, 'M': 35, 'L': 25, 'W': 5}
}
size
: Number of "bots" that will participate to the simulation. The higher, the slower the simulation will be, and the more RAM it will require. 1000 needs about 4GB of RAM.cohorts
: Percentages of users' profiles split between S, M, L and W.
User Traffic
user_traffic_params = [ 3, 16, 19, 21, 23, 25, 26, 28, 30, 31, 33, 34, 36, 38, 40, 42, 45, 48, 51, 54, 59, 65, 74, 88, 200 ]
The array contains a distribution of the number of users expected to be active on the protocol on a given day.
Lending Market
lending_market_params = {
'defi_backend': {
'aave_v3':{
'STABLE': {'max_ltv': 0.75, 'liq_threshold': 0.80,
'liq_penalty': 0.045, 'liq_part': 0.5},
'WBTC': { 'max_ltv': 0.73, 'liq_threshold': 0.78,
'liq_penalty': 0.050, 'liq_part': 0.5 },
'WETH': { 'max_ltv': 0.80, 'liq_threshold': 0.83,
'liq_penalty': 0.050, 'liq_part': 0.5 }
}}
}
Pretty self-explanatory, these parameters reflect the lending protocol's LTV thresholds and liquidation fraction.
Trading Environment
trading_market_params = {
'background_slippage': {
'sizes': { 's': 50000, 'm': 250000, 'l': 1000000 },
'base_fee': { 'btc': 0.0002, 'eth': 0.0002 },
'impact': {
'btc': { 's': 0.0005, 'm': 0.0015, 'l': 0.0030, 'w': 0.0050 },
'eth': { 's': 0.0005, 'm': 0.0015, 'l': 0.0030, 'w': 0.0050 } },
'stress_factor': 1
},
'foreground_slippage': {
'sizes': { 's': 50000, 'm': 250000, 'l': 1000000 },
'stress_factor': 1
}
}
This section defines assumptions about the market environment, in particular about slippage for now.
The
background_slippage
part sets the assumptions about the background trading execution that doesn't need to happen instantly. Trades are classified by size depending on the thresholds; then a base fee is applied plus a price impact per token. Finally, a stress factor allows to multiply the result by any number to increase or decrease it easily in the config.The
foreground_slippage
part sets the size thresholds to classify the trades by size, and a stress factor that allows to multiply the result by any number to increase or decrease it easily in the config. The actual slippage costs for this category of trades will be pulled from the historical statistics included with the price trajectories.
Concrete internal parameters
Decisioning
decisioning_params = {
'promised_amount_fraction':1.0,
'num_disbursements': 3,
'max_ltv_factor': 0.99,
'close_tolerance': 0.05,
'daily_limit': 1000000,
'policy_type': 'OCL',
'policy_duration': 30
}
These parameters are the key factors that are used when approving or declining a policy, and also proposing a PROMISED_AMOUNT and NUM_DISBURSEMENTS policy terms (See Policy Definition for more details on the terms in general.)
promised_amount_fraction:
The fraction of themaximum_amount
that we can promise the user. Themaximum_amount
is the forecasted value of the position at liquidation threshold. If we disbursed the user maximum_amount, when lending protocol liquidation approaches, we would still expect to get it back; while the user remainder would be zero. With that, ifmaximum_amount=$20k
andpromised_amount_fraction=0.8,
we will promise them 0.8 x 20k = $16k.num_disbursements:
All proposed policies will have this term.max_ltv_factor:
If max_ltv is the maximum allowed by the lending protocol, we will only accept new policies when the position's LTV is undermax_ltv_factor *
max_ltvclose_tolerance:
When OCL option is selected, this defines the buffer to keep and foreclose the position if at risk of losses. When EC is selected, this parameter is ignored.daily_limit:
The maximum amount that can be promised per day.policy_type:
OCL for overcollatrelaized; EC for Extended Coverage.policy_duration:
Duration of our coverage policy.
The actual function that performs policy approvals inside the simulation is well separated from the rest of the simulation engine; so experimentation can also be done by modifying the approve
function in the PolicyDecisioning class.
Pricing
pricing_params = {
'ltv_tranches': [0.15, 0.10, 0.05],
'tokens': {
'eth': {
'open_fee': [0.0050, 0.0050, 0.0050, 0.0050],
'interest_rate': 0.2500,
'claim_fee': 0.0075,
'foreclosure_penalty': 0.0250 },
'btc': ...
}
}
Pretty simple here so far, but will evolve into something more sophisticated in the near future.
ltv_tranches
: LTVs thresholds to classify policies at start, these are expressed as deltas under the max ltv allowed by the lending protocol to start a position.open_fee
: percentage of promised amount charged as opening fee for each LTV tranche.interest_rate
: APY charged on the amount owed when a protection is triggered and the promised amount is actually lent. Compounds every second.claim_fee
: Fee charged when we inject collateral into the position to protect it. The user will receive the promised amount converted to collateral using current oracle price, minus the claim_fee percentage.foreclosure_penalty
: is the penalty applied when foreclosing the loan at expiration or other business rule; when the user doesn't repay their loan by themselves. It uses the oracle price for all the foreclosed collateral and applies the penalty percentage.
Vault Params
vault_params = {
'initial_capital': { 'stable': 0, 'btc': 200, 'eth': 0 }
}
initial_capital
: Amounts that the simulated vaults will start with in each token.
Portfolio Params
portfolio_params = {
'shift_drops_claim': 0.00,
'shift_drops_foreclosure': 0.01,
'claim_multiplier': 1.05,
'foreclosure_multiplier': 0.95,
}
shift_drops_claim
: Shifts to apply to drop in prices when calculating claim cash flows.shift_drops_foreclosure
: Shifts to apply to drop in prices when calculating foreclosure cash flows.claim_multiplier
: Multiplier to apply to the claim cash flows.foreclosure_multiplier
: Multiplier to apply to the foreclosure cash flows.
See policy_portfolio.py for more details on the logic implemented to measure the claim and foreclosure future cash flows and how they're used in the simulation.
Buffer Strategy
buffer_strategy_params = {
'price_drop_buy' : 0.0100 ,
'price_drop_sell' : 0.0250 ,
'almost_expired': 1,
'claim_need_multiplier': 1.00,
'foreclosure_need_multiplier': 1.00, }
price_drop_buy
: We will buy enough collateral to be able to service the claims that will be generated by this drop in price.price_drop_sell
: Once we bought collateral, if we have more than enough to service all the way to this drop in price, we will sell the extra collateral.almost_expired
: Number of days that the se consider small enough to prepare for foreclosure.claim_need_multiplier
and
claim_need_multiplier:
Safety multipliers for the amounts to be bought. To compensate for the case where we under-estimated the amount of collateral to buy because the price went down too fast.
Note that price_drop_sell
must be greater than price_drop_buy
to allow for a spread between buying and selling. If they're equal, for example both set at 5%, it would mean that if we need to service 100 ETH in case of a 5% drop in price, we would buy and make sure we have them in the vault. But if the price increases by 0.1%, and reduces the need sharply to 50 ETH only, we would then sell 50 ETH, then buy it again if the price drops by 0.1%, etc. By adding a buffer between buying and selling, we stabilize our reserves and avoid over-trading.
Yield Router
mm_router_params = {
'upper_bound': 0.9,
'lower_bound': 0.85,
'max_participation': 0.05 }
upper_bound
: Maximum percentage of available liquidity to deploy into yield generating markets.lower_bound
: Minimum percentage of available liquidity to deploy into yield generating markets.max_participation
: Maximum percentage that our protocol should weight in one single money market pool or other yield generating protocol.
Engine Params
engine_params = {
'num_cpu': 30,
'min_step_change': 0.005,
'agent_logs': 0.01,
'concat_files_every': 50
}
num_cpu:
Allows running the scenarios in parallel when multiple CPUs are available. Recommended setting is total available CPUs minus 2.min_step_change:
The engine simulates hourly steps, but if all steps are simulated, it has a big memory footprint and will take a long time to run a scenario of one year. To optimize for RAM and speed, we can skip the steps until there's enough change in price. For example, by setting min_step_change to 0.005, some hourly steps will be skipped and will divide RAM footprint and speed up the experiment by a factor of 2 to 3x.agent_logs:
fraction of agents to generate detailed logs for. For example, setting to 0.01 means only 1% of agents will have full detailed logs. This allows to reduce the amount of data generated and the RAM footprint.concat_files_every:
As the simulation progresses in running scenarios, it will aggregate the results every n files.
Running The Simulation
Once all these ingredients are defined, running a simulation is as simple as packaging all the parameters into the inputs folder and starting it.
Example notebooks:
Last updated