├── .gitattributes ├── .gitignore ├── README.md ├── agents ├── __init__.py ├── arbitrageur.py ├── banker.py ├── centralbank.py ├── marketmaker.py ├── marketplayer.py ├── merchant.py ├── nominshorter.py ├── randomizer.py └── speculator.py ├── core ├── __init__.py ├── cache_handler.py ├── model.py ├── orderbook.py ├── server.py ├── settingsloader.py └── stats.py ├── experiments.ipynb ├── managers ├── __init__.py ├── agentmanager.py ├── feemanager.py ├── havvenmanager.py ├── marketmanager.py └── mint.py ├── pytest.ini ├── requirements.txt ├── reset.py ├── run.py ├── test ├── __init__.py ├── test_agents │ ├── __init__.py │ ├── test_arbitrageur.py │ ├── test_banker.py │ ├── test_centralbank.py │ ├── test_marketplayer.py │ ├── test_nominshorter.py │ └── test_randomizer.py ├── test_managers │ ├── __init__.py │ ├── test_agentmanager.py │ ├── test_feemanager.py │ ├── test_havvenmanager.py │ ├── test_marketmanager.py │ └── test_mint.py ├── test_model.py ├── test_orderbook.py ├── test_server.py └── test_stats.py └── visualization ├── __init__.py ├── cached_server.py ├── modules ├── __init__.py ├── bargraph.py ├── candlestick.py ├── chart_visualization.py ├── orderbook_depth.py ├── text_visualization.py └── wealth_graphs.py ├── realtime_server.py ├── templates ├── cache_template.html ├── css │ ├── bootstrap-slider.css │ ├── bootstrap-slider.min.css │ ├── bootstrap-switch.css │ ├── bootstrap-switch.min.css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap-theme.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ └── visualization.css ├── favicon.ico ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── havven_description.html ├── js │ ├── BarGraphModule.js │ ├── CandleStickModule.js │ ├── ChartModule.js │ ├── DepthGraphModule.js │ ├── TextModule.js │ ├── cachedruncontrol.js │ ├── runcontrol.js │ └── static │ │ ├── boost.js │ │ ├── bootstrap-slider.js │ │ ├── bootstrap-slider.min.js │ │ ├── bootstrap-switch.js │ │ ├── bootstrap-switch.min.js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── highcharts.js │ │ ├── highcharts.js.map │ │ ├── highstock.js │ │ ├── highstock.js.map │ │ ├── jquery.min.js │ │ └── jquery.nicescroll.js └── modular_template.html ├── text_visualization.py ├── userparam.py └── visualization_element.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.min.js linguist-generated=true 2 | 3 | visualization/ linguist-vendored 4 | visualization/ModularVisualization.py linguist-vendored=false 5 | visualization/templates/modular_template.html linguist-vendored=false 6 | visualization/templates/js/BarGraphModule.js linguist-vendored=false 7 | visualization/templates/js/ChartModule.js linguist-vendored=false 8 | visualization/templates/js/DepthGraphModule.js linguist-vendored=false 9 | visualization/templates/js/runcontrol.js linguist-vendored=false 10 | visualization/modules/BarGraph.py linguist-vendored=false 11 | visualization/modules/OrderBookDepth.py linguist-vendored=false 12 | visualization/modules/Wealth.py linguist-vendored=false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Settings file, generated by settingsloader.py 2 | settings.ini 3 | # cache data file 4 | cache_data.pkl 5 | 6 | # Python runtime files 7 | *.pyc 8 | __pycache__ 9 | 10 | # Python library/util caches 11 | .ipynb_checkpoints 12 | .mypy_cache 13 | .cache 14 | 15 | # IDE, editor, and OS caches 16 | # (these should probably be in ./git/info/exclude) 17 | .idea 18 | .vscode 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Havven Simulation 2 | 3 | ## Running the simulation 4 | 5 | This will be an agent-based model of the Havven system. 6 | 7 | Before it can be run, you will need Python 3.6 or later and to install the pre-requisites with pip: 8 | 9 | ```pip3 install -r requirements.txt``` 10 | 11 | To run the simulation, invoke: 12 | 13 | ```python3 run.py``` 14 | 15 | If it is your first time running the simulation, a `settings.ini` file will be generated, that will give you control over how the simulation will run. 16 | The first settings `cached = True` determines whether the data will be pre-generated before spawning a server, or whether the data will be generated in real time. 17 | More info about the settings and caching can be found in the relevant sections. 18 | 19 | To open the experiments notebook: 20 | 21 | ```jupyter notebook experiments.ipynb``` 22 | 23 | To run the tests: 24 | 25 | ```python3 -m pytest --pyargs -v``` 26 | 27 | Note: Running pytest through python3 is more consistent (global pytest install, other python versions). 28 | The -v flag is for verbose, to list every individual test passing. 29 | 30 | ## Settings 31 | 32 | Settings are contained in `settings.ini`, the file will be generated on the first run of the simulation using the `python3 run.py` command, individual setting descriptions can be found in `settingsloader.py`. 33 | 34 | ## Caching 35 | 36 | Changing the caching setting changes how the data will be generated before being displayed on the local webpage. If caching is true, the data will be generated beforehand, and will be sent to the client at a rate only limited by connection speed (the `fps_default` setting controls this). 37 | Otherwise, data will be presented in real time, being generated by the server as quickly as the client can request the next step. 38 | 39 | Another difference between the two is the changing of model settings. If caching is true, settings are determined by dataset settings found in `cache_handler.py`. With caching being false, settings can be changed on the client side, and then generated by the server with the new settings. 40 | 41 | ## Overview 42 | 43 | There are three major components to this simulation: 44 | 45 | * The currency environment of Havven itself; 46 | * A virtual exchange to go between nomins, havvens, and fiat; 47 | * The agents themselves. possible future players: 48 | - [x] random players 49 | - [x] arbitrageurs 50 | - [x] havven bankers 51 | - [x] central bankers 52 | - [x] merchants / consumers 53 | - [x] market makers 54 | - [x] buy-and-hold speculators 55 | - [ ] day-trading speculators 56 | - [ ] cryptocurrency refugees 57 | - [ ] attackers 58 | 59 | ## Technicals 60 | It runs on [Mesa](https://github.com/projectmesa/mesa), and includes the following files and folders: 61 | 62 | * `run.py` - the main entry point for the program 63 | * `reset.py` - script to clear and reset settings to default, and regenerate cache 64 | * `server.py` - the simulation and visualisation server are instantiated here 65 | * `model.py` - the actual ABM of Havven itself 66 | * `core/orderbook.py` - an order book class for constructing markets between the three main currencies 67 | * `core/stats.py` - statistical functions for examining interesting economic properties of the Havven model 68 | * `core/settingsloader.py` - loads and generates settings files 69 | * `core/cache_handler.py` - cached datasets are generated and loaded by this module 70 | * `managers/` - helper classes for managing the Havven model's various parts 71 | * `agents/` - economic actors who will interact with the model and the order book 72 | * `test/` - the test suite 73 | * `visualization/` - facilities for producing a live visualization web page 74 | * `experiments.ipynb` - an environment for exploring system dynamics and scenarios in a more-efficient offline fashion. 75 | -------------------------------------------------------------------------------- /agents/__init__.py: -------------------------------------------------------------------------------- 1 | from .marketplayer import MarketPlayer 2 | from .arbitrageur import Arbitrageur 3 | from .banker import Banker 4 | from .randomizer import Randomizer 5 | from .centralbank import CentralBank 6 | from .speculator import HavvenSpeculator, NaiveSpeculator 7 | from .nominshorter import NominShorter, HavvenEscrowNominShorter 8 | from .merchant import Merchant, Buyer 9 | from .marketmaker import MarketMaker 10 | 11 | # player names for the UI sliders 12 | player_names = { 13 | # 'CentralBank': CentralBank, 14 | 'Arbitrageur': Arbitrageur, 15 | 'Banker': Banker, 16 | 'Randomizer': Randomizer, 17 | 'NominShorter': NominShorter, 18 | 'HavvenEscrowNominShorter': HavvenEscrowNominShorter, 19 | 'HavvenSpeculator': HavvenSpeculator, 20 | 'NaiveSpeculator': NaiveSpeculator, 21 | 'Merchant': Merchant, 22 | 'Buyer': Buyer, 23 | 'MarketMaker': MarketMaker 24 | } 25 | 26 | # exclude players when showing profit % 27 | players_to_exclude = ["Merchant", "Buyer"] 28 | -------------------------------------------------------------------------------- /agents/banker.py: -------------------------------------------------------------------------------- 1 | import random 2 | from decimal import Decimal as Dec 3 | from typing import Optional, Tuple 4 | 5 | from managers import HavvenManager as hm 6 | from core import orderbook as ob 7 | from .marketplayer import MarketPlayer 8 | 9 | 10 | class Banker(MarketPlayer): 11 | """Wants to buy havvens and issue nomins, in order to accrue fees.""" 12 | 13 | def __init__(self, *args, **kwargs) -> None: 14 | super().__init__(*args, **kwargs) 15 | self.fiat_havven_order: Optional[Tuple[int, "ob.Bid"]] = None 16 | """The time the order was placed as well as the fiat/hvn order""" 17 | self.nomin_havven_order: Optional[Tuple[int, "ob.Bid"]] = None 18 | self.nomin_fiat_order: Optional[Tuple[int, "ob.Ask"]] = None 19 | self.sell_rate: Dec = hm.round_decimal(Dec(random.random()/3 + 0.1)) 20 | self.trade_premium: Dec = Dec('0.01') 21 | self.trade_duration: int = 10 22 | # step when initialised so nomins appear on the market. 23 | self.step() 24 | 25 | def setup(self, init_value: Dec): 26 | endowment = hm.round_decimal(init_value * Dec(4)) 27 | self.fiat = init_value 28 | self.model.endow_havvens(self, endowment) 29 | 30 | def step(self) -> None: 31 | if self.nomin_havven_order is not None: 32 | if self.model.manager.time >= self.nomin_havven_order[0] + self.trade_duration: 33 | self.nomin_havven_order[1].cancel() 34 | self.nomin_havven_order = None 35 | if self.nomin_fiat_order is not None: 36 | if self.model.manager.time >= self.nomin_fiat_order[0] + self.trade_duration: 37 | self.nomin_fiat_order[1].cancel() 38 | self.nomin_fiat_order = None 39 | if self.fiat_havven_order is not None: 40 | if self.model.manager.time >= self.fiat_havven_order[0] + self.trade_duration: 41 | self.fiat_havven_order[1].cancel() 42 | self.fiat_havven_order = None 43 | 44 | if self.available_nomins > 0: 45 | if len(self.model.datacollector.model_vars['0']) > 0: 46 | havven_supply = self.model.datacollector.model_vars['Havven Supply'][-1] 47 | fiat_supply = self.model.datacollector.model_vars['Fiat Supply'][-1] 48 | # buy into the market with more supply, as by virtue of there being more supply, 49 | # the market will probably have a better price... 50 | if havven_supply > fiat_supply: 51 | order = self.place_havven_nomin_bid_with_fee( 52 | self.available_nomins*self.sell_rate, 53 | self.havven_nomin_market.price * (Dec(1)-self.trade_premium) 54 | ) 55 | if order is None: 56 | return 57 | self.nomin_havven_order = ( 58 | self.model.manager.time, 59 | order 60 | ) 61 | 62 | else: 63 | order = self.place_nomin_fiat_ask_with_fee( 64 | self.available_nomins*self.sell_rate, 65 | self.nomin_fiat_market.price * (Dec(1)+self.trade_premium) 66 | ) 67 | if order is None: 68 | return 69 | self.nomin_fiat_order = ( 70 | self.model.manager.time, 71 | order 72 | ) 73 | 74 | if self.available_fiat > 0 and not self.fiat_havven_order: 75 | order = self.place_havven_fiat_bid_with_fee( 76 | hm.round_decimal(self.available_fiat * self.sell_rate), 77 | self.havven_fiat_market.price * (Dec(1)-self.trade_premium) 78 | ) 79 | if order is None: 80 | return 81 | self.fiat_havven_order = ( 82 | self.model.manager.time, 83 | order 84 | ) 85 | 86 | if self.available_havvens > 0: 87 | self.escrow_havvens(self.available_havvens) 88 | 89 | issuable = self.max_issuance_rights() - self.issued_nomins 90 | if hm.round_decimal(issuable) > 0: 91 | self.issue_nomins(issuable) 92 | -------------------------------------------------------------------------------- /agents/centralbank.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | from typing import Optional 3 | 4 | from managers import HavvenManager as hm 5 | from .marketplayer import MarketPlayer 6 | 7 | 8 | class CentralBank(MarketPlayer): 9 | """Attempts to use its cash reserves to stabilise prices at a certain level.""" 10 | 11 | def __init__(self, unique_id: int, havven_model: "model.HavvenModel", 12 | fiat: Dec = Dec(0), havvens: Dec = Dec(0), 13 | nomins: Dec = Dec(0), 14 | havven_target: Optional[Dec] = None, 15 | nomin_target: Optional[Dec] = None, 16 | havven_nomin_target: Optional[Dec] = None, 17 | tolerance: Dec = Dec('0.01')) -> None: 18 | 19 | super().__init__(unique_id, havven_model, fiat=fiat, 20 | havvens=havvens, nomins=nomins) 21 | 22 | # Note: it only really makes sense to target one of these at a time. 23 | # And operating on the assumption that arbitrage is working in the market. 24 | 25 | self.havven_target = havven_target 26 | """The targeted havven/fiat price.""" 27 | 28 | self.nomin_target = nomin_target 29 | """The targeted nomin/fiat price.""" 30 | 31 | # TODO: Actually use this 32 | self.havven_nomin_target = havven_nomin_target 33 | """The targeted havven/nomin exchange rate.""" 34 | 35 | self.tolerance = tolerance 36 | """The bank will try to correct the price if it strays out of this range.""" 37 | 38 | def step(self) -> None: 39 | 40 | self.cancel_orders() 41 | 42 | if self.havven_target is not None: 43 | havven_price = self.havven_fiat_market.price 44 | # Price is too high, it should decrease: we will sell havvens at a discount. 45 | if havven_price > hm.round_decimal(self.havven_target * (Dec(1) + self.tolerance)): 46 | # If we have havvens, sell them. 47 | if self.havvens > 0: 48 | self.place_havven_fiat_ask_with_fee(self._fraction(self.havvens), 49 | self.havven_target) 50 | else: 51 | # If we do not have havvens, but we have some escrowed which we can 52 | # immediately free, free them. 53 | available_havvens = self.available_escrowed_havvens() 54 | if available_havvens > 0: 55 | self.unescrow_havvens(self._fraction(available_havvens)) 56 | # If we have some nomins we could burn to free up havvens, burn them. 57 | if self.unavailable_escrowed_havvens() > 0: 58 | # If we have nomins, then we should burn them. 59 | if self.available_nomins > 0: 60 | self.burn_nomins(self._fraction(self.available_nomins)) 61 | # Otherwise, we should buy some to burn, if we can. 62 | elif self.available_fiat > 0: 63 | self.sell_fiat_for_nomins_with_fee(self._fraction(self.available_fiat)) 64 | 65 | # Price is too low, it should increase: we will buy havvens at a premium. 66 | elif havven_price < hm.round_decimal(self.havven_target * (Dec(1) - self.tolerance)): 67 | # Buy some if we have fiat to buy it with. 68 | if self.available_fiat > 0: 69 | self.place_havven_fiat_bid_with_fee(self._fraction(self.available_fiat), 70 | self.havven_target) 71 | else: 72 | # If we have some nomins, sell them for fiat 73 | if self.available_nomins > 0: 74 | self.sell_nomins_for_fiat_with_fee(self._fraction(self.available_nomins)) 75 | else: 76 | # If we have some havvens we could escrow to get nomins, escrow them. 77 | if self.available_havvens > 0: 78 | self.escrow_havvens(self._fraction(self.available_havvens)) 79 | # If we have remaining issuance capacity, then issue some nomins to sell. 80 | issuance_rights = self.remaining_issuance_rights() 81 | if issuance_rights > 0: 82 | self.issue_nomins(issuance_rights) 83 | 84 | if self.nomin_target is not None: 85 | nomin_price = self.nomin_fiat_market.price 86 | # Price is too high, it should decrease: we will sell nomins at a discount. 87 | if nomin_price > hm.round_decimal(self.nomin_target * (Dec(1) + self.tolerance)): 88 | if self.available_nomins > 0: 89 | self.place_nomin_fiat_ask_with_fee(self._fraction(self.available_nomins), 90 | self.nomin_target) 91 | else: 92 | # If we have some havvens, we can issue nomins on the back of them to sell. 93 | if self.available_havvens > 0: 94 | self.escrow_havvens(self._fraction(self.available_havvens)) 95 | issuance_rights = self.remaining_issuance_rights() 96 | if issuance_rights > 0: 97 | self.issue_nomins(issuance_rights) 98 | # Otherwise, obtain some. 99 | else: 100 | self.sell_fiat_for_havvens_with_fee(self._fraction(self.available_fiat)) 101 | 102 | # Price is too low, it should increase: we will buy nomins at a premium. 103 | elif nomin_price < hm.round_decimal(self.nomin_target * (Dec(1) - self.tolerance)): 104 | if self.available_fiat > 0: 105 | self.place_nomin_fiat_bid_with_fee(self._fraction(self.available_fiat), 106 | self.nomin_target) 107 | else: 108 | if self.available_havvens > 0: 109 | self.sell_havvens_for_fiat_with_fee(self._fraction(self.available_havvens)) 110 | else: 111 | # If we do not have havvens, but we have some escrowed which we can 112 | # immediately free, free them. 113 | available_havvens = self.available_escrowed_havvens() 114 | if available_havvens > 0: 115 | self.unescrow_havvens(self._fraction(available_havvens)) 116 | # If we have some nomins we could burn to free up havvens, burn them. 117 | if self.unavailable_escrowed_havvens() > 0: 118 | # If we have nomins, then we should burn them. 119 | if self.available_nomins > 0: 120 | self.burn_nomins(self._fraction(self.available_nomins)) 121 | -------------------------------------------------------------------------------- /agents/marketmaker.py: -------------------------------------------------------------------------------- 1 | """ 2 | http://www.eecs.harvard.edu/cs286r/courses/fall12/papers/OPRS10.pdf 3 | http://www.cs.cmu.edu/~aothman/ 4 | """ 5 | 6 | import random 7 | from decimal import Decimal as Dec 8 | from typing import Dict, Any, Optional 9 | 10 | from agents import MarketPlayer 11 | from core import orderbook as ob 12 | 13 | 14 | class MarketMaker(MarketPlayer): 15 | """ 16 | Market makers in general have four desiderata: 17 | (1) bounded loss 18 | (2) the ability to make a profit 19 | (3) a vanishing bid/ask spread 20 | (4) unlimited market depth 21 | 22 | This market maker uses Fixed prices with shrinking profit cut 23 | 24 | Probably the simplest automated market maker is to determine a probability distribution 25 | over the future states of the world, and to offer to make bets directly at those odds. 26 | 27 | If we allow the profit cut to diminish to zero as trading volume increases, the resulting 28 | market maker has three of the four desired properties: the ability to make a profit, 29 | a vanishing marginal bid/ask spread, and unbounded depth in limit. 30 | 31 | However, it still has unbounded worst-case loss because a trader with knowledge of the 32 | true future could make an arbitrarily large winning bet with the market maker. 33 | 34 | To calculate the probability of the future state, a simple gradient on the moving average 35 | will be used. 36 | 37 | In the case where the market maker believes the price will rise, it will place a sell at a 38 | set price in the for the future, and slowly buy at higher and higher prices 39 | | _--_ 40 | | ======== (moon) 41 | | ======== --- "--" 42 | |== ----- 43 | | ----- === 44 | | ----- === 45 | |-- === 46 | | === 47 | | === 48 | |=== 49 | | -> Time 50 | = Market maker's bid/ask spread 51 | - Predicted price movement 52 | the price difference is dependant on the gradient 53 | """ 54 | def __init__(self, *args, **kwargs) -> None: 55 | super().__init__(*args, **kwargs) 56 | self.last_bet_end: int = random.randint(-20, 10) 57 | '''How long since the last market maker's "bet"''' 58 | 59 | self.minimal_wait: int = 10 60 | '''How long will the market maker wait since it's last "bet" to let the market move on its own''' 61 | 62 | self.bet_duration: int = 30 63 | '''How long will the market maker do its "bet" for''' 64 | 65 | self.bet_percentage: Dec = Dec('1') 66 | ''' 67 | How much of it's wealth will the market maker use on a "bet" 68 | The bot will constantly update the orders on either side to use all it's wealth 69 | multiplied by the percentage value 70 | ''' 71 | 72 | self.initial_bet_margin: Dec = Dec('0.05') 73 | ''' 74 | How off the expected price gradient will the bets be placed initially 75 | ''' 76 | 77 | self.ending_bet_margin: Dec = Dec('0.01') 78 | ''' 79 | How close the bets get at the end 80 | ''' 81 | 82 | self.trade_market = random.choice([ 83 | self.havven_fiat_market, 84 | self.nomin_fiat_market, 85 | self.havven_nomin_market 86 | ]) 87 | 88 | self.current_bet: Optional[Dict[str, Any[str, int, Dec, 'ob.LimitOrder']]] = None 89 | 90 | @property 91 | def name(self) -> str: 92 | return f"{self.__class__.__name__} {self.unique_id} ({self.trade_market.name})" 93 | 94 | def setup(self, init_value) -> None: 95 | """ 96 | Initially give the players the two currencies of their trade_market 97 | if they trade in nomins, start with fiat instead, to purchase the nomins 98 | """ 99 | if self.trade_market == self.havven_fiat_market: 100 | self.fiat = init_value*Dec(3) 101 | self.model.endow_havvens(self, init_value*Dec(3)) 102 | if self.trade_market == self.havven_nomin_market: 103 | self.fiat = init_value*Dec(4) 104 | self.model.endow_havvens(self, init_value*Dec(2)) 105 | if self.trade_market == self.nomin_fiat_market: 106 | self.fiat = init_value*Dec(6) 107 | 108 | def step(self) -> None: 109 | # don't do anything until only holding the correct two currencies 110 | if self.trade_market == self.havven_nomin_market: 111 | if self.available_fiat > 0: 112 | self.sell_fiat_for_havvens_with_fee(self.available_fiat / Dec(2)) 113 | self.sell_fiat_for_nomins_with_fee(self.available_fiat / Dec(2)) 114 | if self.available_fiat > 1: 115 | return 116 | if self.trade_market == self.nomin_fiat_market: 117 | if self.available_havvens > 0: 118 | self.sell_havvens_for_fiat_with_fee(self.available_nomins / Dec(2)) 119 | self.sell_havvens_for_nomins_with_fee(self.available_nomins / Dec(2)) 120 | if self.available_havvens > 1: 121 | return 122 | if self.trade_market == self.havven_fiat_market: 123 | if self.available_nomins > 0: 124 | self.sell_nomins_for_fiat_with_fee(self.available_nomins / Dec(2)) 125 | self.sell_nomins_for_havvens_with_fee(self.available_nomins / Dec(2)) 126 | if self.available_nomins > 1: 127 | return 128 | 129 | # if the duration has ended, close the trades 130 | if self.last_bet_end >= self.minimal_wait + self.bet_duration: 131 | self.last_bet_end = 0 132 | self.current_bet['bid'].cancel() 133 | self.current_bet['ask'].cancel() 134 | self.current_bet = None 135 | # if the duration hasn't ended, update the trades 136 | elif self.current_bet is not None: 137 | # update both bid and ask every step in case orders were partially filled 138 | # so that quantities are updated 139 | self.current_bet['bid'].cancel() 140 | self.current_bet['ask'].cancel() 141 | bid = self.place_bid_func( 142 | self.last_bet_end-self.minimal_wait, 143 | self.current_bet['gradient'], 144 | self.current_bet['initial_price'] 145 | ) 146 | if bid is None: 147 | self.current_bet = None 148 | self.last_bet_end = 0 149 | return 150 | ask = self.place_ask_func( 151 | self.last_bet_end-self.minimal_wait, 152 | self.current_bet['gradient'], 153 | self.current_bet['initial_price'] 154 | ) 155 | if ask is None: 156 | bid.cancel() 157 | self.current_bet = None 158 | self.last_bet_end = 0 159 | return 160 | self.current_bet['bid'] = bid 161 | self.current_bet['ask'] = ask 162 | 163 | # if the minimal wait period has ended, create a bet 164 | elif self.last_bet_end >= self.minimal_wait: 165 | self.last_bet_end = self.minimal_wait 166 | gradient = self.calculate_gradient(self.trade_market) 167 | if gradient is None: 168 | return 169 | start_price = self.trade_market.price 170 | 171 | bid = self.place_bid_func( 172 | 0, 173 | gradient, 174 | start_price 175 | ) 176 | if bid is None: 177 | return 178 | 179 | ask = self.place_ask_func( 180 | 0, 181 | gradient, 182 | start_price 183 | ) 184 | if ask is None: 185 | bid.cancel() 186 | return 187 | 188 | self.current_bet = { 189 | 'gradient': gradient, 190 | 'initial_price': start_price, 191 | 'bid': bid, 192 | 'ask': ask 193 | } 194 | self.last_bet_end += 1 195 | 196 | def place_bid_func(self, time_in_effect: int, gradient: Dec, start_price: Dec) -> "ob.Bid": 197 | """ 198 | Place a bid at a price dependent on the time in effect and gradient 199 | based on the player's margins 200 | 201 | The price chosen is the current predicted price (start + gradient * time_in_effect) 202 | multiplied by the current bet margin 1-(fraction of time remaining)*(max-min margin)+min_margin 203 | """ 204 | price = start_price + Dec(gradient * time_in_effect) 205 | multiplier = 1 - ( 206 | (Dec((self.bet_duration - time_in_effect) / self.bet_duration) * 207 | (self.initial_bet_margin-self.ending_bet_margin) 208 | ) + self.ending_bet_margin 209 | ) 210 | if self.trade_market == self.nomin_fiat_market: 211 | return self.place_nomin_fiat_bid_with_fee( 212 | self.available_fiat*self.bet_percentage/price, 213 | price*multiplier 214 | ) 215 | elif self.trade_market == self.havven_fiat_market: 216 | return self.place_havven_fiat_bid_with_fee( 217 | self.available_fiat*self.bet_percentage/price, 218 | price*multiplier 219 | ) 220 | elif self.trade_market == self.havven_nomin_market: 221 | return self.place_havven_nomin_bid_with_fee( 222 | self.available_havvens*self.bet_percentage/price, 223 | price*multiplier 224 | ) 225 | 226 | def place_ask_func(self, time_in_effect: int, gradient: Dec, start_price: Dec) -> "ob.Ask": 227 | """ 228 | Place an ask at a price dependent on the time in effect and gradient 229 | based on the player's margins 230 | 231 | The price chosen is the current predicted price (start + gradient * time_in_effect) 232 | multiplied by the current bet margin 1+(fraction of time remaining)*(max-min margin)+min_margin 233 | """ 234 | price = start_price + Dec(gradient*time_in_effect) 235 | multiplier = 1 + ( 236 | (Dec((self.bet_duration - time_in_effect) / self.bet_duration) * 237 | (self.initial_bet_margin-self.ending_bet_margin) 238 | ) + self.ending_bet_margin 239 | ) 240 | if self.trade_market == self.nomin_fiat_market: 241 | return self.place_nomin_fiat_ask_with_fee(self.available_nomins*self.bet_percentage, price*multiplier) 242 | elif self.trade_market == self.havven_fiat_market: 243 | return self.place_havven_fiat_ask_with_fee(self.available_havvens*self.bet_percentage, price*multiplier) 244 | elif self.trade_market == self.havven_nomin_market: 245 | return self.place_havven_nomin_ask_with_fee(self.available_nomins*self.bet_percentage, price*multiplier) 246 | 247 | def calculate_gradient(self, trade_market: 'ob.OrderBook') -> Optional[Dec]: 248 | """ 249 | Calculate the gradient of the moving average by taking the difference of the last two points 250 | """ 251 | if len(trade_market.price_data) < 2: 252 | return None 253 | return (trade_market.price_data[-1] - trade_market.price_data[-2])/2 254 | -------------------------------------------------------------------------------- /agents/merchant.py: -------------------------------------------------------------------------------- 1 | """ 2 | merchant.py 3 | 4 | A merchant who owns an inventory and restocks it using fiat, 5 | and sells the goods for nomins. 6 | 7 | A buyer who receives a wage in fiat, and uses that to purchase 8 | goods with nomins. 9 | 10 | The result on the market will be a consistent conversion from 11 | fiat to nomins by the buyers, and a bulk conversion from nomins 12 | to fiat by the merchants. 13 | """ 14 | 15 | from .marketplayer import MarketPlayer 16 | from decimal import Decimal as Dec 17 | from collections import defaultdict 18 | 19 | from typing import Dict 20 | import random 21 | 22 | 23 | class Merchant(MarketPlayer): 24 | """ 25 | A merchant market player, this represents someone who 26 | sells goods/services for nomins. 27 | 28 | Inventory/restocking will be dealt with using fiat. 29 | 30 | Starts with an initial inventory, stock level goal, 31 | and updates their stock every few ticks. 32 | 33 | As nomin->fiat fee is a percentage, transfer all nomins to fiat 34 | every step. 35 | """ 36 | 37 | def __init__(self, *args, **kwargs) -> None: 38 | super().__init__(*args, **kwargs) 39 | 40 | # Set up this merchant's inventory of items, their stocks, and their prices. 41 | self.inventory: Dict[str, Dict[str, Dec]] = { 42 | # name: price(nomins), stock_price(fiat), current_stock, stock_goal 43 | str(i): {'price': Dec(random.random() * 20)+1, 'stock_price': Dec(1), 44 | 'current_stock': Dec(100), 'stock_goal': Dec(100)} 45 | for i in range(1, random.randint(4, 6)) 46 | } 47 | for i in self.inventory: 48 | self.inventory[i]['stock_price'] = self.inventory[i]['price'] * Dec((random.random() / 3) + 0.5) 49 | 50 | self.last_restock: int = 0 51 | """Time since the last inventory restock.""" 52 | 53 | self.restock_tick_rate: int = random.randint(20, 30) 54 | """Time between inventory restocking. Randomised to prevent all merchants restocking at once.""" 55 | 56 | def setup(self, init_value: Dec): 57 | self.fiat = init_value 58 | 59 | def step(self) -> None: 60 | self.last_restock += 1 61 | if self.last_restock > self.restock_tick_rate: 62 | self.last_restock = 0 63 | self.sell_nomins_for_fiat_with_fee(self.available_nomins) 64 | 65 | for item in self.inventory: 66 | info = self.inventory[item] 67 | to_restock = info['stock_goal'] - info['current_stock'] 68 | cost = to_restock*info['stock_price'] 69 | if self.available_fiat > cost: 70 | self.fiat -= cost 71 | self.inventory[item]['current_stock'] += to_restock 72 | # if out of money try again in 2 ticks. 73 | else: 74 | amount_possible = int(self.available_fiat / info['stock_price']) 75 | self.fiat -= info['stock_price']*amount_possible 76 | self.inventory[item]['current_stock'] += amount_possible 77 | 78 | def sell_stock(self, agent: 'Buyer', item: str, quantity: Dec) -> Dec: 79 | """ 80 | Function to transfer stock to buyer, telling the buyer how much they 81 | need to transfer... We can trust the buyer will transfer. 82 | """ 83 | if agent.available_nomins > self.inventory[item]['price']*quantity and \ 84 | self.inventory[item]['current_stock'] > quantity: 85 | self.inventory[item]['current_stock'] -= quantity 86 | return self.inventory[item]['price']*quantity 87 | return Dec(0) 88 | 89 | 90 | class Buyer(MarketPlayer): 91 | """ 92 | Buyer interacts with merchants to purchase goods using nomins. 93 | Buyers receive a wage in fiat, buy nomins, and then use them to buy goods. 94 | """ 95 | min_wage = 2 96 | max_wage = 10 97 | min_mpc = 0.1 98 | max_mpc = 0.9 99 | 100 | def __init__(self, *args, **kwargs) -> None: 101 | super().__init__(*args, **kwargs) 102 | 103 | self.inventory = defaultdict(Dec) 104 | self.wage = random.randint(self.min_wage, self.max_wage) 105 | 106 | self.mpc = (self.max_mpc - self.min_mpc) * random.random() + self.min_mpc 107 | """This agent's marginal propensity to consume.""" 108 | 109 | def setup(self, init_value: Dec): 110 | self.fiat = init_value 111 | 112 | def step(self) -> None: 113 | # Earn some dough. 114 | self.fiat += self.wage 115 | 116 | # Buy some crypto. 117 | if self.available_fiat: 118 | self.sell_fiat_for_nomins_with_fee(self.available_fiat) 119 | 120 | # If feeling spendy, buy something. 121 | if random.random() < self.mpc: 122 | to_buy = Dec(int(random.random()*5)+1) 123 | buying_from = random.choice(self.model.agent_manager.agents['Merchant']) 124 | buying = random.choice(list(buying_from.inventory.keys())) 125 | amount = buying_from.sell_stock(self, buying, Dec(to_buy)) 126 | if amount > 0: 127 | self.transfer_nomins_to(buying_from, amount) 128 | self.inventory[buying] += to_buy 129 | -------------------------------------------------------------------------------- /agents/nominshorter.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | from typing import Tuple, Optional 3 | 4 | from managers import HavvenManager 5 | from core import orderbook as ob 6 | from .marketplayer import MarketPlayer 7 | 8 | 9 | class NominShorter(MarketPlayer): 10 | """ 11 | Holds onto nomins until the nomin->fiat price is favourable 12 | Then trades nomins for fiat and holds until the nomin price stabilises 13 | Then trades fiat for nomins 14 | 15 | Primary aim is to increase amount of nomins 16 | 17 | TODO: Rates should be set based on fees (should try to make at least 0.5% or so on each cycle), 18 | Also rates could be set based on some random factor per player that can dictate 19 | the upper limit, lower limit and gap between limits 20 | 21 | 22 | TODO: Maybe put up a wall by placing nomin ask @ sell_rate_threshold 23 | """ 24 | initial_order = None 25 | 26 | _nomin_sell_rate_threshold = Dec('1.03') 27 | """The rate above which the player will sell nomins""" 28 | 29 | _nomin_buy_rate_threshold = Dec('0.99') 30 | """The rate below which the player will buy nomins""" 31 | 32 | def setup(self, init_value: Dec): 33 | self.fiat = init_value * Dec(2) 34 | 35 | def step(self) -> None: 36 | # get rid of havvens, as that isn't the point of this player 37 | if self.available_havvens: 38 | self.sell_havvens_for_nomins(self.available_havvens) 39 | 40 | if self.available_nomins > 0: 41 | trade = self._find_best_nom_fiat_trade() 42 | last_price = 0 43 | while trade is not None and self.available_nomins > 0: 44 | if last_price == trade[0]: 45 | print("nom", self.unique_id, trade, self.portfolio()) 46 | break 47 | last_price = trade[0] 48 | ask = self._make_nom_fiat_trade(trade) 49 | trade = self._find_best_nom_fiat_trade() 50 | 51 | if self.available_fiat > 0: 52 | trade = self._find_best_fiat_nom_trade() 53 | last_price = 0 54 | while trade is not None and self.available_fiat > 0: 55 | # rarely, the entire order doesn't get filled, still trying to debug... 56 | if last_price == trade[0]: 57 | break 58 | last_price = trade[0] 59 | bid = self._make_fiat_nom_trade(trade) 60 | trade = self._find_best_fiat_nom_trade() 61 | 62 | def _find_best_nom_fiat_trade(self) -> Optional[Tuple[Dec, Dec]]: 63 | trade_price_quant = None 64 | for bid in self.nomin_fiat_market.highest_bids(): 65 | if bid.price < self._nomin_sell_rate_threshold: 66 | break 67 | if trade_price_quant is not None: 68 | trade_price_quant = (bid.price, trade_price_quant[1] + bid.quantity) 69 | else: 70 | trade_price_quant = (bid.price, bid.quantity) 71 | return trade_price_quant 72 | 73 | def _make_nom_fiat_trade(self, trade_price_quant: Tuple[Dec, Dec]) -> "ob.Ask": 74 | fee = self.model.fee_manager.transferred_nomins_fee(trade_price_quant[1]) 75 | # if not enough nomins to cover whole ask 76 | if self.available_nomins < trade_price_quant[1] + fee: 77 | return self.sell_nomins_for_fiat_with_fee(self.available_nomins) 78 | return self.place_nomin_fiat_ask(trade_price_quant[1], trade_price_quant[0]) 79 | 80 | def _find_best_fiat_nom_trade(self) -> Optional[Tuple[Dec, Dec]]: 81 | trade_price_quant = None 82 | for ask in self.nomin_fiat_market.lowest_asks(): 83 | if ask.price > self._nomin_buy_rate_threshold: 84 | break 85 | if trade_price_quant is not None: 86 | trade_price_quant = (ask.price, trade_price_quant[1] + ask.quantity) 87 | else: 88 | trade_price_quant = (ask.price, ask.quantity) 89 | return trade_price_quant 90 | 91 | def _make_fiat_nom_trade(self, trade_price_quant: Tuple[Dec, Dec]) -> "ob.Bid": 92 | fee = self.model.fee_manager.transferred_fiat_fee(trade_price_quant[1]) 93 | # if not enough fiat to cover whole bid 94 | if self.available_fiat < trade_price_quant[1] + fee: 95 | return self.sell_fiat_for_nomins_with_fee(self.available_fiat) 96 | return self.place_nomin_fiat_bid(trade_price_quant[1], trade_price_quant[0]) 97 | 98 | 99 | class HavvenEscrowNominShorter(NominShorter): 100 | """ 101 | Escrows havvens for nomins when the rate of nom->fiat is favourable 102 | then waits for market to stabilise, trusting that nomin price will go back 103 | to 1 104 | 105 | havvens-(issue)->nomins->fiat(and wait)->nomin-(burn)->havvens+extra nomins 106 | 107 | This should profit on the issuing and burning mechanics (if they scale 108 | with the price), the nomin/fiat trade and accruing fees 109 | 110 | In the end this player should hold escrowed havvens and nomins left over that he 111 | can't burn 112 | """ 113 | def setup(self, init_value: Dec): 114 | self.havvens = init_value * Dec(2) 115 | self.fiat = init_value 116 | 117 | def step(self) -> None: 118 | # keep all havvens escrowed to make issuing nomins easier 119 | if self.available_havvens > 0: 120 | self.escrow_havvens(self.available_havvens) 121 | 122 | nomins = self.available_nomins + self.remaining_issuance_rights() 123 | 124 | if nomins > 0: 125 | trade = self._find_best_nom_fiat_trade() 126 | last_price = 0 127 | while trade is not None and HavvenManager.round_decimal(nomins) > 0: 128 | if last_price == trade[0]: 129 | break 130 | last_price = trade[0] 131 | self._issue_nomins_up_to(trade[1]) 132 | ask = self._make_nom_fiat_trade(trade) 133 | trade = self._find_best_nom_fiat_trade() 134 | nomins = self.available_nomins + self.remaining_issuance_rights() 135 | 136 | if self.available_fiat > 0: 137 | trade = self._find_best_fiat_nom_trade() 138 | last_price = 0 139 | while trade is not None and HavvenManager.round_decimal(self.available_fiat) > 0: 140 | if last_price == trade[0]: 141 | break 142 | last_price = trade[0] 143 | bid = self._make_fiat_nom_trade(trade) 144 | trade = self._find_best_fiat_nom_trade() 145 | 146 | if self.issued_nomins: 147 | if self.available_nomins < self.issued_nomins: 148 | self.burn_nomins(self.available_nomins) 149 | else: 150 | self.burn_nomins(self.issued_nomins) 151 | 152 | def _issue_nomins_up_to(self, quantity: Dec) -> bool: 153 | """ 154 | If quantity > currently issued nomins, including fees to trade, issue more nomins 155 | 156 | If the player cant issue more nomins than the quantity, 157 | """ 158 | fee = HavvenManager.round_decimal(self.model.fee_manager.transferred_nomins_fee(quantity)) 159 | 160 | # if there are enough nomins, return 161 | if self.available_nomins > fee + quantity: 162 | return True 163 | 164 | nomins_needed = fee + quantity - self.available_nomins 165 | 166 | if self.remaining_issuance_rights() > nomins_needed: 167 | return self.issue_nomins(nomins_needed) 168 | else: 169 | return self.issue_nomins(self.remaining_issuance_rights()) 170 | -------------------------------------------------------------------------------- /agents/randomizer.py: -------------------------------------------------------------------------------- 1 | """agents.py: Individual agents that will interact with the Havven market.""" 2 | import random 3 | from decimal import Decimal as Dec 4 | 5 | from core import orderbook as ob 6 | from managers import HavvenManager as hm 7 | from .marketplayer import MarketPlayer 8 | 9 | 10 | class Randomizer(MarketPlayer): 11 | """Places random bids and asks near current market prices.""" 12 | 13 | def __init__(self, unique_id: int, havven_model: "model.HavvenModel", 14 | fiat: Dec = Dec(0), 15 | havvens: Dec = Dec(0), 16 | nomins: Dec = Dec(0), 17 | variance: Dec = Dec(0.02), 18 | order_lifetime: int = 30, 19 | max_orders: int = 10) -> None: 20 | super().__init__(unique_id, havven_model, fiat, havvens, nomins) 21 | self.variance = variance 22 | """This agent will place orders within (+/-)variance*price of the going rate.""" 23 | 24 | self.order_lifetime = order_lifetime 25 | """Orders older than this lifetime will be cancelled.""" 26 | 27 | self.max_orders = max_orders 28 | """Don't submit more than this number of orders.""" 29 | 30 | def setup(self, init_value: Dec): 31 | self.fiat = init_value 32 | self.model.endow_havvens(self, Dec(3) * init_value) 33 | 34 | def step(self) -> None: 35 | # Cancel expired orders 36 | condemned = [] 37 | for order in self.orders: 38 | if order.book.time > order.time + self.order_lifetime: 39 | condemned.append(order) 40 | for order in condemned: 41 | order.cancel() 42 | 43 | if len(self.orders) < self.max_orders: 44 | action = random.choice([self._havven_fiat_bid, self._havven_fiat_ask, 45 | self._nomin_fiat_bid, self._nomin_fiat_ask, 46 | self._havven_nomin_bid, self._havven_nomin_ask]) 47 | if action() is None: 48 | return 49 | 50 | def _havven_fiat_bid(self) -> "ob.Bid": 51 | price = self.havven_fiat_market.price 52 | movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) 53 | return self.place_havven_fiat_bid(self._fraction(self.available_fiat, Dec(10)), price + movement) 54 | 55 | def _havven_fiat_ask(self) -> "ob.Ask": 56 | price = self.havven_fiat_market.price 57 | movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) 58 | return self.place_havven_fiat_ask(self._fraction(self.available_havvens, Dec(10)), price + movement) 59 | 60 | def _nomin_fiat_bid(self) -> "ob.Bid": 61 | price = self.nomin_fiat_market.price 62 | movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) 63 | return self.place_nomin_fiat_bid(self._fraction(self.available_fiat, Dec(10)), price + movement) 64 | 65 | def _nomin_fiat_ask(self) -> "ob.Ask": 66 | price = self.nomin_fiat_market.price 67 | movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) 68 | return self.place_nomin_fiat_ask(self._fraction(self.available_nomins, Dec(10)), price + movement) 69 | 70 | def _havven_nomin_bid(self) -> "ob.Bid": 71 | price = self.havven_nomin_market.price 72 | movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) 73 | return self.place_havven_nomin_bid(self._fraction(self.available_nomins, Dec(10)), price + movement) 74 | 75 | def _havven_nomin_ask(self) -> "ob.Ask": 76 | price = self.havven_nomin_market.price 77 | movement = hm.round_decimal(Dec(2*random.random() - 1) * price * self.variance) 78 | return self.place_havven_nomin_ask(self._fraction(self.available_havvens, Dec(10)), price + movement) 79 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import * 3 | -------------------------------------------------------------------------------- /core/cache_handler.py: -------------------------------------------------------------------------------- 1 | """ 2 | cache_handler.py 3 | 4 | Functions to help with loading and generating caches of model runs 5 | given certain parameters. 6 | 7 | This should work hand-in-hand with CachedServer to allow users to view 8 | these cached runs, without using large amounts of server resources by 9 | generating new data per user. 10 | """ 11 | 12 | import pickle 13 | 14 | import tqdm 15 | 16 | from core import model 17 | from core import settingsloader 18 | 19 | 20 | run_settings = [ 21 | # settings for each individual run to create a cache for. 22 | # name: having a "Default" run is required 23 | # - all names have to be unique 24 | # max_steps: required, and ignore whatever is in settings.ini 25 | # settings: change the defaults set in settings.ini, per run 26 | # - any settings that are not in settings.ini are ignored 27 | { 28 | "name": "Balanced", 29 | "description": """

This dataset runs the simulation with the default model settings, 30 | viewable in settings.

The agent fractions have been selected to be a good balance of what 31 | could be expected in a real system. For more information about what the actors represent, read the 32 | About section.

""", 33 | "max_steps": 1000, 34 | "settings": { 35 | "Model": { 36 | 'num_agents': 100 37 | } 38 | } 39 | }, 40 | { 41 | "name": "Low collateralisation ratio, high issuance", 42 | "description": """

This dataset highlights how the collateralisation ratio affects volatility 43 | and the nomin supply.

A low collateralisation ratio max (0.1) limits the supply of nomins quite harshly 44 | in relation to the havven price. This scenario requires the havven price to rise extremely quickly 45 | in order to allow for enough nomins to be created, if nomin demand is to be met and the price kept at 1.

""", 46 | "max_steps": 1000, 47 | "settings": { 48 | "Model": { 49 | 'num_agents': 125, 50 | "utilisation_ratio_max": 0.1 51 | }, 52 | "AgentFractions": { 53 | "Banker": 100 54 | } 55 | } 56 | }, 57 | { 58 | "name": "Default collateralisation ratio, high issuance", 59 | "description": """

This dataset highlights how the collateralisation ratio affects volatility 60 | and the nomin supply.

The default collateralisation ratio max (0.25) creates a balanced 61 | constraint in the supply of nomins. This situation requires the havven price to rise faster than 62 | the nomin demand to allow for enough nomins to be created to keep the price at 1, without 63 | flooding the nomin market.

""", 64 | "max_steps": 1000, 65 | "settings": { 66 | "Model": { 67 | 'num_agents': 125, 68 | }, 69 | "AgentFractions": { 70 | "Banker": 100 71 | } 72 | } 73 | }, 74 | { 75 | "name": "High collateralisation ratio, high issuance", 76 | "description": """

This dataset, highlights how the collateralisation ratio affects volatility 77 | and nomin supply.

The high collateralisation ratio max (0.5) 78 | allows for the issuance of a large amount of nomins, which causes the intrinsic value of havvens to skyrocket 79 | as the value they can issue is so high. This creates massive oversupply of nomins, crashing the price.

""", 80 | "max_steps": 1000, 81 | "settings": { 82 | "Model": { 83 | 'num_agents': 125, 84 | "utilisation_ratio_max": 0.5 85 | }, 86 | "AgentFractions": { 87 | "Banker": 100 88 | } 89 | } 90 | }, 91 | { 92 | "name": "Many random actors", 93 | "description": """

This noisy dataset that examines how well the price stays at 1 94 | even with many actors behaving irrationally (or arationally).

""", 95 | "max_steps": 1000, 96 | "settings": { 97 | "Model": { 98 | 'num_agents': 125, 99 | }, 100 | "AgentFractions": { 101 | "Randomizer": 100 102 | } 103 | } 104 | }, 105 | { 106 | "name": "Minimal", 107 | "description": """

This dataset contains a single member of each market player 108 | type that exists in the system. This highlights how they interact with each other.

""", 109 | "max_steps": 1000, 110 | "settings": { 111 | "Model": { 112 | 'num_agents': 0, 113 | }, 114 | "Agents": { 115 | 'agent_minimum': 1 116 | } 117 | } 118 | }, 119 | { 120 | "name": "Low number of Nomin Shorters", 121 | "description": """

This dataset removes a lot of the price control the nomin shorters bring, 122 | to see how well the price stays at 1 without the user expectation that the price would be near 1.

123 |

This will show whether controlling the supply is enough to keep the price stable.

""", 124 | "max_steps": 1000, 125 | "settings": { 126 | "Model": { 127 | 'num_agents': 125, 128 | }, 129 | "AgentFractions": { 130 | "NominShorter": 0, 131 | "HavvenEscrowNominShorter": 0 132 | } 133 | } 134 | }, 135 | ] 136 | 137 | 138 | def generate_new_caches(data): 139 | """ 140 | generate a new dataset for each dataset that doesn't already exist in data 141 | 142 | overwrites the defined default settings for every run 143 | 144 | generate visualisation results for every step up to max_steps, and save it to 'result' 145 | 146 | store the result in the format: 147 | data["name"] = {"data": result, "settings": settings, "max_steps": max_steps} 148 | """ 149 | from core.server import get_vis_elements 150 | 151 | for n, item in enumerate(run_settings): 152 | if item["name"] in data and len(data[item['name']]['data']) == item['max_steps']: 153 | print("already have:", item['name']) 154 | continue 155 | print("\nGenerating", item["name"]) 156 | result = [] 157 | settings = settingsloader.get_defaults() 158 | 159 | for section in item["settings"]: 160 | for setting in item['settings'][section]: 161 | settings[section][setting] = item["settings"][section][setting] 162 | 163 | model_settings = settings['Model'] 164 | model_settings['agent_fractions'] = settings['AgentFractions'] 165 | 166 | havven_model = model.HavvenModel( 167 | model_settings, 168 | settings['Fees'], 169 | settings['Agents'], 170 | settings['Havven'] 171 | ) 172 | vis_elements = get_vis_elements() 173 | 174 | # # The following is for running the loop without tqdm 175 | # # as when profiling the model tqdm shows up as ~17% runtime 176 | # for i in range(item["max_steps"]): 177 | # if not i % 100: 178 | # print(f"{n+1}/{len(run_settings)} [{'='*(i//100)}{'-'*(item['max_steps']//100 - i//100)}" + 179 | # f"] {i}/{item['max_steps']}") 180 | 181 | for i in tqdm.tqdm(range(item["max_steps"])): 182 | havven_model.step() 183 | step_data = [] 184 | for element in vis_elements: 185 | if i == 0: 186 | if hasattr(element, "sent_data"): 187 | element.sent_data = False 188 | element_data = element.render(havven_model) 189 | else: 190 | element_data = element.render(havven_model) 191 | else: 192 | element_data = element.render(havven_model) 193 | step_data.append(element_data) 194 | 195 | result.append(step_data) 196 | data[item["name"]] = { 197 | "data": result, 198 | "settings": settings, 199 | "max_steps": item["max_steps"], 200 | "description": item["description"] 201 | } 202 | return data 203 | 204 | 205 | def load_saved(): 206 | try: 207 | with open("./cache_data.pkl", 'rb') as f: 208 | print("Loading from cache_data.pkl...") 209 | data = pickle.load(f) 210 | except IOError: 211 | data = {} 212 | except EOFError: 213 | data = {} 214 | return data 215 | 216 | 217 | def save_data(data): 218 | """overwrite existing cache file with the presented data""" 219 | with open("./cache_data.pkl", "wb") as f: 220 | pickle.dump(data, f) 221 | print("Caches saved to cache_data.pkl") 222 | -------------------------------------------------------------------------------- /core/model.py: -------------------------------------------------------------------------------- 1 | """model.py: The Havven model itself lives here.""" 2 | 3 | from decimal import Decimal as Dec 4 | from typing import Dict, Any 5 | 6 | from mesa import Model 7 | from mesa.time import RandomActivation 8 | 9 | import agents as ag 10 | from managers import (HavvenManager, MarketManager, 11 | FeeManager, Mint, 12 | AgentManager) 13 | from core import stats 14 | 15 | 16 | class HavvenModel(Model): 17 | """ 18 | An agent-based model of the Havven stablecoin system. This class will 19 | provide the basic market functionality of Havven, an exchange, and a 20 | place for the market agents to live and interact. 21 | The aim is to stabilise the nomin price, but we would also like to measure 22 | other quantities including liquidity, volatility, wealth concentration, 23 | velocity of money and so on. 24 | """ 25 | def __init__(self, 26 | model_settings: Dict[str, Any], 27 | fee_settings: Dict[str, Any], 28 | agent_settings: Dict[str, Any], 29 | havven_settings: Dict[str, Any]) -> None: 30 | """ 31 | 32 | :param model_settings: Setting that are modifiable on the frontend 33 | - agent_fraction: what percentage of each agent to use 34 | - num_agents: the total number of agents to use 35 | - utilisation_ratio_max: the max utilisation ratio for nomin issuance against havvens 36 | - continuous_order_matching: whether to match orders as they come, 37 | or at the end of each tick 38 | :param fee_settings: explained in feemanager.py 39 | :param agent_settings: explained in agentmanager.py 40 | :param havven_settings: explained in havvenmanager.py 41 | """ 42 | agent_fractions = model_settings['agent_fractions'] 43 | num_agents = model_settings['num_agents'] 44 | utilisation_ratio_max = model_settings['utilisation_ratio_max'] 45 | continuous_order_matching = model_settings['continuous_order_matching'] 46 | 47 | # Mesa setup. 48 | super().__init__() 49 | 50 | # The schedule will activate agents in a random order per step. 51 | self.schedule = RandomActivation(self) 52 | 53 | # Set up data collection. 54 | self.datacollector = stats.create_datacollector() 55 | 56 | # Initialise simulation managers. 57 | self.manager = HavvenManager( 58 | Dec(utilisation_ratio_max), 59 | continuous_order_matching, 60 | havven_settings 61 | ) 62 | self.fee_manager = FeeManager( 63 | self.manager, 64 | fee_settings 65 | ) 66 | self.market_manager = MarketManager(self.manager, self.fee_manager) 67 | self.mint = Mint(self.manager, self.market_manager) 68 | 69 | self.agent_manager = AgentManager( 70 | self, 71 | num_agents, 72 | agent_fractions, 73 | agent_settings 74 | ) 75 | 76 | def fiat_value(self, havvens=Dec('0'), nomins=Dec('0'), 77 | fiat=Dec('0')) -> Dec: 78 | """Return the equivalent fiat value of the given currency basket.""" 79 | return self.market_manager.havvens_to_fiat(havvens) + \ 80 | self.market_manager.nomins_to_fiat(nomins) + fiat 81 | 82 | def endow_havvens(self, agent: "ag.MarketPlayer", havvens: Dec) -> None: 83 | """Grant an agent an endowment of havvens.""" 84 | if havvens > 0: 85 | value = min(self.manager.havvens, havvens) 86 | agent.havvens += value 87 | self.manager.havvens -= value 88 | 89 | def step(self) -> None: 90 | """Advance the model by one step.""" 91 | # Agents submit trades. 92 | self.schedule.step() 93 | 94 | self.market_manager.havven_nomin_market.step_history() 95 | self.market_manager.havven_fiat_market.step_history() 96 | self.market_manager.nomin_fiat_market.step_history() 97 | 98 | # Resolve outstanding trades. 99 | if not self.manager.continuous_order_matching: 100 | self.market_manager.havven_nomin_market.match() 101 | self.market_manager.havven_fiat_market.match() 102 | self.market_manager.nomin_fiat_market.match() 103 | 104 | # Distribute fees periodically. 105 | if (self.manager.time % self.fee_manager.fee_period) == 0: 106 | self.fee_manager.distribute_fees(self.schedule.agents) 107 | 108 | # Collect data. 109 | self.datacollector.collect(self) 110 | 111 | # Advance Time Itself. 112 | self.manager.time += 1 113 | -------------------------------------------------------------------------------- /core/server.py: -------------------------------------------------------------------------------- 1 | """server.py: Functions for setting up the simulation/visualisation server.""" 2 | 3 | from typing import List 4 | 5 | import tornado.web 6 | 7 | import agents 8 | from core import settingsloader, model 9 | from visualization.cached_server import CachedModularServer 10 | from visualization.modules import ChartModule, OrderBookModule, WealthModule, PortfolioModule, \ 11 | CurrentOrderModule, CandleStickModule, PastOrdersModule 12 | from visualization.realtime_server import ModularServer 13 | from visualization.userparam import UserSettableParameter 14 | from visualization.visualization_element import VisualizationElement 15 | 16 | 17 | def get_vis_elements() -> List[VisualizationElement]: 18 | ref_colour = "lightgrey" 19 | 20 | profit_colors = ["blue", "red", "green", "orchid", "darkorchid", "fuchsia", "purple", 21 | "teal", "darkorange", "darkkaki", "darkgoldenrod", "slategrey", "seagreen"] 22 | 23 | profit_percentage_lines = [ 24 | {"Label": "Avg Profit %", "Color": "grey"}, 25 | ] 26 | 27 | for n, name in enumerate([i for i in agents.player_names if i not in agents.players_to_exclude]): 28 | profit_percentage_lines.append({"Label": name, "Color": profit_colors[n]}) 29 | 30 | return [ 31 | ChartModule( 32 | profit_percentage_lines, 33 | desc="Each market player group's profit as a percentage of initial wealth.", 34 | title="Profitability per Strategy", 35 | group="Player Aggregate Stats" 36 | ), 37 | 38 | CandleStickModule( 39 | [ 40 | { 41 | "Label": "NominFiatPriceData", "orderbook": "NominFiatOrderBook", 42 | "AvgColor": "rgba(0,191,255,0.6)", "VolumeColor": "rgba(0,191,255,0.3)", # deepskyblue 43 | } 44 | ], 45 | desc="Candlesticks, rolling price average and volume for the nomin/fiat market.", 46 | title="Nomin/Fiat Market Price", 47 | group="Market Prices" 48 | ), 49 | 50 | CandleStickModule( 51 | [ 52 | { 53 | "Label": "HavvenFiatPriceData", "orderbook": "HavvenFiatOrderBook", 54 | "AvgColor": "rgba(255,0,0,0.6)", "VolumeColor": "rgba(255,0,0,0.3)", # red 55 | } 56 | ], 57 | desc="Candlesticks, rolling price average and volume for the havven/fiat market.", 58 | title="Havven/Fiat Market Price", 59 | group="Market Prices" 60 | ), 61 | 62 | CandleStickModule( 63 | [ 64 | { 65 | "Label": "HavvenNominPriceData", "orderbook": "HavvenNominOrderBook", 66 | "AvgColor": "rgba(153,50,204,0.6)", "VolumeColor": "rgba(153,50,204,0.3)", # darkorchid 67 | } 68 | ], 69 | desc="Candlesticks, rolling price average and volume for the nomin/fiat market.", 70 | title="Havven/Nomin Market Price", 71 | group="Market Prices" 72 | ), 73 | # 74 | # # ChartModule([ 75 | # # {"Label": "Max Wealth", "Color": "purple"}, 76 | # # {"Label": "Min Wealth", "Color": "orange"}, 77 | # # ]), 78 | 79 | PortfolioModule( 80 | [{"Label": "WealthBreakdown"}], 81 | fiat_values=False, 82 | desc="Player Portfolios", 83 | title="Wealth Breakdown", 84 | group="Player Wealth", 85 | ), 86 | 87 | WealthModule( 88 | [{"Label": "Wealth"}], 89 | desc="Individual market player's holdings in terms of fiat.", 90 | title="Player Net Worth", 91 | group="Player Wealth" 92 | ), 93 | 94 | ChartModule( 95 | [{"Label": "Gini", "Color": "navy"}], # {"Label": "0", "Color": ref_colour} 96 | desc="Income inequality metric: increases from 0 to 1 as inequality does.", 97 | title="Gini Coefficient", 98 | group="Player Wealth" 99 | ), 100 | 101 | CurrentOrderModule( 102 | [{"Label": "PlayerBidAskVolume"}], 103 | desc="Each market player's bids and asks, for each market.", 104 | title="Outstanding Player Orders", 105 | group="Player Orders" 106 | ), 107 | 108 | PastOrdersModule( 109 | [{"Label": "TotalMarketVolume"}], 110 | desc="Each market player's bids and asks that were filled, for each market.", 111 | title="Total Player Order Volume", 112 | group="Player Orders" 113 | ), 114 | 115 | ChartModule( 116 | [ 117 | {"Label": "Havven Demand", "Color": "red"}, 118 | {"Label": "Havven Supply", "Color": "orange"}, 119 | ], 120 | desc="The aggregate demand and supply of havvens in the markets.", 121 | title="Havven Order Volume", 122 | group="Supply and Demand" 123 | ), 124 | 125 | ChartModule([ 126 | {"Label": "Nomin Demand", "Color": "purple"}, 127 | {"Label": "Nomin Supply", "Color": "deepskyblue"}, 128 | ], 129 | desc="The aggregate demand and supply of nomins in the markets.", 130 | title="Nomin Order Volume", 131 | group="Supply and Demand" 132 | ), 133 | 134 | ChartModule([ 135 | {"Label": "Fiat Demand", "Color": "darkgreen"}, 136 | {"Label": "Fiat Supply", "Color": "lightgreen"}, 137 | ], 138 | desc="The aggregate demand and supply of fiat in the markets.", 139 | title="Fiat Order Volume", 140 | group="Supply and Demand" 141 | ), 142 | 143 | ChartModule([ 144 | {"Label": "Nomins", "Color": "deepskyblue"}, 145 | {"Label": "Escrowed Havvens", "Color": "darkred"}, 146 | ], 147 | desc="The total number of nomins and escrowed havvens for all market players.", 148 | title="Nomins to Escrowed Havvens", 149 | group="Issuance" 150 | ), 151 | 152 | ChartModule([ 153 | {"Label": "Fee Pool", "Color": "blue"}, 154 | ], 155 | desc="The amount of fees collected by the system, that haven't yet been distributed.", 156 | title="Collected Fees", 157 | group="Fees" 158 | ), 159 | 160 | ChartModule([ 161 | {"Label": "Fees Distributed", "Color": "blue"}, 162 | ], 163 | desc="Total amount of fees that have been distributed by the system.", 164 | title="Distributed Fees", 165 | group="Fees" 166 | ), 167 | # 168 | # ChartModule([ 169 | # {"Label": "Havven Nomins", "Color": "deepskyblue"}, 170 | # {"Label": "Havven Havvens", "Color": "red"}, 171 | # {"Label": "Havven Fiat", "Color": "darkgreen"}, 172 | # ]), 173 | # 174 | 175 | OrderBookModule( 176 | [{"Label": "NominFiatOrderBook"}], 177 | desc="The nomin/fiat market order book (tallied bid/ask volume by price).", 178 | title="Nomin/Fiat Order Book", 179 | group="Order Books" 180 | ), 181 | 182 | OrderBookModule( 183 | [{"Label": "HavvenFiatOrderBook"}], 184 | desc="The havven/fiat market order book (tallied bid/ask volume by price).", 185 | title="Havven/Fiat Order Book", 186 | group="Order Books" 187 | ), 188 | 189 | OrderBookModule( 190 | [{"Label": "HavvenNominOrderBook"}], 191 | desc="The Havven/Nomin market order book (tallied bid/ask volume by price).", 192 | title="Havven/Nomin Order Book", 193 | group="Order Books" 194 | ) 195 | ] 196 | 197 | 198 | def make_server() -> "tornado.web.Application": 199 | """ 200 | Set up the simulation/visualisation server and return it. 201 | 202 | "Label": "0"/"1" is a workaround to show the graph label where there is only one label 203 | (the graphs with only one label wont show the label value, and also show multiple 204 | values at the same time) 205 | """ 206 | settings = settingsloader.load_settings() 207 | 208 | charts: List[VisualizationElement] = get_vis_elements() 209 | 210 | if settings["Server"]["cached"]: 211 | print("Running cached data server...") 212 | 213 | server = CachedModularServer(settings, charts, "Havven Model (Alpha)") 214 | 215 | else: 216 | print("Running model server...") 217 | 218 | n_slider = UserSettableParameter( 219 | 'slider', "Number of agents", 220 | settings["Model"]["num_agents"], settings["Model"]["num_agents_min"], 221 | settings["Model"]["num_agents_max"], 1 222 | ) 223 | 224 | ur_slider = UserSettableParameter( 225 | 'slider', "Utilisation Ratio", settings["Model"]["utilisation_ratio_max"], 0.0, 1.0, 0.01 226 | ) 227 | 228 | match_checkbox = UserSettableParameter( 229 | 'checkbox', "Continuous order matching", settings["Model"]["continuous_order_matching"] 230 | ) 231 | 232 | if settings['Model']['random_agents']: 233 | agent_fraction_selector = UserSettableParameter( 234 | 'agent_fractions', "Agent fraction selector", None 235 | ) 236 | else: 237 | # the none value will randomize the data on every model reset 238 | agent_fraction_selector = UserSettableParameter( 239 | 'agent_fractions', "Agent fraction selector", settings['AgentFractions'] 240 | ) 241 | 242 | server = ModularServer( 243 | settings, 244 | model.HavvenModel, 245 | charts, 246 | "Havven Model (Alpha)", 247 | { 248 | "num_agents": n_slider, "utilisation_ratio_max": ur_slider, 249 | "continuous_order_matching": match_checkbox, 250 | 'agent_fractions': agent_fraction_selector 251 | } 252 | ) 253 | return server 254 | -------------------------------------------------------------------------------- /core/settingsloader.py: -------------------------------------------------------------------------------- 1 | """ 2 | settingsloader.py 3 | 4 | Loads settings from the settings file if it exists, otherwise generates a new 5 | one with certain defaults. 6 | """ 7 | import configparser 8 | import os.path 9 | import copy 10 | 11 | 12 | def get_defaults(): 13 | settings = { 14 | 'Server': { 15 | # TODO: whether to used cached results or not 16 | 'cached': True, 17 | 18 | # whether to run the model in a separate thread for each socket connection 19 | # it runs worse with threading, so better to just leave it as false... 20 | 'threaded': False, 21 | 'port': 3000, 22 | 'fps_max': 15, # max fps for the model to run at 23 | 'fps_default': 15, 24 | 'max_steps': 1500 # max number of steps to generate up to 25 | }, 26 | 'Model': { 27 | 'num_agents_max': 175, 28 | 'num_agents_min': 20, 29 | 'num_agents': 50, 30 | 31 | # ignore Agent Fractions, and choose random figures 32 | 'random_agents': False, 33 | 'utilisation_ratio_max': '0.25', 34 | 'continuous_order_matching': True, 35 | }, 36 | 'Fees': { 37 | 'fee_period': 50, 38 | 'stable_nomin_fee_level': '0.005', 39 | 'stable_havven_fee_level': '0.005', 40 | 'stable_fiat_fee_level': '0.005', 41 | 'stable_nomin_issuance_fee': '0', 42 | 'stable_nomin_redemption_fee': '0' 43 | }, 44 | 'Agents': { 45 | 'agent_minimum': 1, 46 | 'wealth_parameter': 1000 47 | }, 48 | 'AgentFractions': { 49 | # these are normalised to total 1 later 50 | 'Arbitrageur': 3, 51 | 'Banker': 25, 52 | 'Randomizer': 15, 53 | 'NominShorter': 15, 54 | 'HavvenEscrowNominShorter': 10, 55 | 'HavvenSpeculator': 6, 56 | 'NaiveSpeculator': 0, 57 | 'Merchant': 0, 58 | 'Buyer': 6, 59 | 'MarketMaker': 20 60 | }, 61 | 'Havven': { 62 | 'havven_supply': '1000000000', 63 | 'nomin_supply': '0', 64 | 'rolling_avg_time_window': 7, 65 | 'use_volume_weighted_avg': True 66 | }, 67 | 'AgentDescriptions': { 68 | "Arbitrageur": "The arbitrageur finds arbitrage cycles and profits off them", 69 | "Banker": "The banker acquires as many Havvens as they can and issues nomins to buy more", 70 | "Randomizer": "The randomizer places random bids and asks on all markets close to the market price", 71 | "NominShorter": "The nomin shorter sells nomins when the price is high and buys when they are low", 72 | "HavvenEscrowNominShorter": "The havven escrow nomin shorters behave the same as the nomin shorters, but aquire nomins through escrowing havvens", 73 | "HavvenSpeculator": "The havven speculator buys havvens hoping the price will appreciate after some period.", 74 | "NaiveSpeculator": "The naive speculator behaves similarly to the havven speculators, but does so on all the markets", 75 | "Merchant": "The merchant provides goods for Buyers, selling them for nomins. They sell the nomins back into fiat", 76 | "Buyer": "The buyers bring fiat into the system systematically, trading it for nomins, to buy goods from the merchant", 77 | "MarketMaker": "The market maker creates liquidity on some market in what they hope to be a profitable manner" 78 | } 79 | 80 | } 81 | return copy.deepcopy(settings) 82 | 83 | 84 | def load_settings(): 85 | settings = get_defaults() 86 | 87 | config = configparser.ConfigParser() 88 | config.optionxform = str # allow for camelcase 89 | 90 | if os.path.exists("settings.ini"): 91 | print("Loading settings from settings.ini") 92 | config.read("settings.ini") 93 | for section in config: 94 | if section not in settings: 95 | if section is not "DEFAULT": 96 | print(f"{section} is not a valid section, skipping.") 97 | continue 98 | for item in config[section]: 99 | if item not in settings[section]: 100 | print(f"{item} is not a valid setting for {section}, skipping.") 101 | continue 102 | if type(settings[section][item]) == str: 103 | settings[section][item] = config[section][item] 104 | elif type(settings[section][item]) == int: 105 | try: 106 | settings[section][item] = config.getint(section, item) 107 | except ValueError: 108 | print( 109 | f''' 110 | Expected int for ({section}, {item}), got value "{config.get(section, item)}" 111 | Using default value of: {settings[section][item]} 112 | ''' 113 | ) 114 | elif type(settings[section][item]) == bool: 115 | try: 116 | settings[section][item] = config.getboolean(section, item) 117 | except ValueError: 118 | print( 119 | f''' 120 | Expected boolean for ({section}, {item}), got value "{config.get(section, item)}" 121 | Using default value of: {settings[section][item]} 122 | ''' 123 | ) 124 | else: 125 | print("No settings.ini file present, creating one with default settings.") 126 | for section in settings: 127 | config.add_section(section) 128 | for item in settings[section]: 129 | config.set(section, item, str(settings[section][item])) 130 | with open("settings.ini", 'w') as f: 131 | config.write(f) 132 | # make all the agent fractions floats based on max 133 | total = sum(settings['AgentFractions'][i] for i in settings['AgentFractions']) 134 | for i in settings['AgentFractions']: 135 | settings['AgentFractions'][i] = settings['AgentFractions'][i]/total 136 | 137 | return settings 138 | -------------------------------------------------------------------------------- /core/stats.py: -------------------------------------------------------------------------------- 1 | """stats.py: Functions for extracting aggregate information from the Havven model.""" 2 | 3 | from statistics import stdev 4 | from typing import List, Any 5 | 6 | from mesa.datacollection import DataCollector 7 | 8 | import agents 9 | 10 | 11 | def mean(values: List[Any]): 12 | if len(values) > 0: 13 | return sum(values)/len(values) 14 | return 0 15 | 16 | 17 | def _profit_excluded(name: str) -> bool: 18 | """ 19 | True iff the agent's profit should be is excluded 20 | from the average profit computation. 21 | """ 22 | return name in agents.players_to_exclude 23 | 24 | 25 | def mean_profit_fraction(havven_model: "model.HavvenModel") -> float: 26 | """ 27 | Return the average fraction of profit being made by market participants, 28 | excluding Merchants and Buyers. 29 | """ 30 | if len(havven_model.schedule.agents) == 0: 31 | return 0 32 | return float(mean([a.profit_fraction() for a in havven_model.schedule.agents 33 | if not _profit_excluded(a)])) 34 | 35 | 36 | def mean_agent_profit_fraction(name: str, havven_model: "model.HavvenModel"): 37 | if len(havven_model.agent_manager.agents[name]) == 0: 38 | return 0 39 | return float(mean([a.profit_fraction() for a in havven_model.agent_manager.agents[name]])) 40 | 41 | 42 | def wealth_sd(havven_model: "model.HavvenModel") -> float: 43 | """Return the standard deviation of wealth in the market.""" 44 | return float(stdev([a.wealth() for a in havven_model.schedule.agents])) 45 | 46 | 47 | def gini(havven_model: "model.HavvenModel") -> float: 48 | """Return the gini coefficient in the market.""" 49 | n = len(havven_model.schedule.agents) 50 | s_wealth = sorted([a.wealth() for a in havven_model.schedule.agents]) 51 | total_wealth = float(sum(s_wealth)) 52 | if total_wealth == 0 or n == 0: 53 | return 0 54 | scaled_wealth = float(sum([(i+1)*w for i, w in enumerate(s_wealth)])) 55 | return (2.0*scaled_wealth)/(n*total_wealth) - (n+1.0)/n 56 | 57 | 58 | def max_wealth(havven_model: "model.HavvenModel") -> float: 59 | """Return the wealth of the richest person in the market.""" 60 | if len(havven_model.schedule.agents) == 0: 61 | return 0 62 | 63 | return float(max([a.wealth() for a in havven_model.schedule.agents])) 64 | 65 | 66 | def min_wealth(havven_model: "model.HavvenModel") -> float: 67 | """Return the wealth of the poorest person in the market.""" 68 | if len(havven_model.schedule.agents) == 0: 69 | return 0 70 | 71 | return float(min([a.wealth() for a in havven_model.schedule.agents])) 72 | 73 | 74 | def fiat_demand(havven_model: "model.HavvenModel") -> float: 75 | """Return the total quantity of fiat presently being bought in the marketplace.""" 76 | havvens = float(sum([ask.quantity * ask.price for ask in havven_model.market_manager.havven_fiat_market.asks])) 77 | nomins = float(sum([ask.quantity * ask.price for ask in havven_model.market_manager.nomin_fiat_market.asks])) 78 | return havvens + nomins 79 | 80 | 81 | def fiat_supply(havven_model: "model.HavvenModel") -> float: 82 | """Return the total quantity of fiat presently being sold in the marketplace.""" 83 | havvens = float(sum([bid.quantity * bid.price for bid in havven_model.market_manager.havven_fiat_market.bids])) 84 | nomins = float(sum([bid.quantity * bid.price for bid in havven_model.market_manager.nomin_fiat_market.bids])) 85 | return havvens + nomins 86 | 87 | 88 | def havven_demand(havven_model: "model.HavvenModel") -> float: 89 | """Return the total quantity of havvens presently being bought in the marketplace.""" 90 | nomins = float(sum([bid.quantity for bid in havven_model.market_manager.havven_nomin_market.bids])) 91 | fiat = float(sum([bid.quantity for bid in havven_model.market_manager.havven_fiat_market.bids])) 92 | return nomins + fiat 93 | 94 | 95 | def havven_supply(havven_model: "model.HavvenModel") -> float: 96 | """Return the total quantity of havvens presently being sold in the marketplace.""" 97 | nomins = float(sum([ask.quantity for ask in havven_model.market_manager.havven_fiat_market.asks])) 98 | fiat = float(sum([ask.quantity for ask in havven_model.market_manager.havven_nomin_market.asks])) 99 | return nomins + fiat 100 | 101 | 102 | def nomin_demand(havven_model: "model.HavvenModel") -> float: 103 | """Return the total quantity of nomins presently being bought in the marketplace.""" 104 | havvens = float(sum([ask.quantity * ask.price for ask in havven_model.market_manager.havven_nomin_market.asks])) 105 | fiat = float(sum([bid.quantity for bid in havven_model.market_manager.nomin_fiat_market.bids])) 106 | return havvens + fiat 107 | 108 | 109 | def nomin_supply(havven_model: "model.HavvenModel") -> float: 110 | """Return the total quantity of nomins presently being sold in the marketplace.""" 111 | havvens = float(sum([bid.quantity * bid.price for bid in havven_model.market_manager.havven_nomin_market.bids])) 112 | fiat = float(sum([ask.quantity for ask in havven_model.market_manager.nomin_fiat_market.asks])) 113 | return havvens + fiat 114 | 115 | 116 | def create_datacollector() -> DataCollector: 117 | base_reporters = { 118 | "0": lambda x: 0, # Note: workaround for showing labels (more info server.py) 119 | "1": lambda x: 1, 120 | "Nomin Price": lambda h: float(h.market_manager.nomin_fiat_market.price), 121 | "Nomin Ask": lambda h: float(h.market_manager.nomin_fiat_market.lowest_ask_price()), 122 | "Nomin Bid": lambda h: float(h.market_manager.nomin_fiat_market.highest_bid_price()), 123 | "Havven Price": lambda h: float(h.market_manager.havven_fiat_market.price), 124 | "Havven Ask": lambda h: float(h.market_manager.havven_fiat_market.lowest_ask_price()), 125 | "Havven Bid": lambda h: float(h.market_manager.havven_fiat_market.highest_bid_price()), 126 | "Havven/Nomin Price": lambda h: float(h.market_manager.havven_nomin_market.price), 127 | "Havven/Nomin Ask": lambda h: float(h.market_manager.havven_nomin_market.lowest_ask_price()), 128 | "Havven/Nomin Bid": lambda h: float(h.market_manager.havven_nomin_market.highest_bid_price()), 129 | "Havven Nomins": lambda h: float(h.manager.nomins), 130 | "Havven Havvens": lambda h: float(h.manager.havvens), 131 | "Havven Fiat": lambda h: float(h.manager.fiat), 132 | "Gini": gini, 133 | "Nomins": lambda h: float(h.manager.nomin_supply), 134 | "Escrowed Havvens": lambda h: float(h.manager.escrowed_havvens), 135 | #"Wealth SD": stats.wealth_sd, 136 | "Max Wealth": max_wealth, 137 | "Min Wealth": min_wealth, 138 | "Avg Profit %": lambda h: round(100 * mean_profit_fraction(h), 3), 139 | "Havven Demand": havven_demand, 140 | "Havven Supply": havven_supply, 141 | "Nomin Demand": nomin_demand, 142 | "Nomin Supply": nomin_supply, 143 | "Fiat Demand": fiat_demand, 144 | "Fiat Supply": fiat_supply, 145 | "Fee Pool": lambda h: float(h.manager.nomins), 146 | "Fees Distributed": lambda h: float(h.fee_manager.fees_distributed), 147 | "NominFiatOrderBook": lambda h: h.market_manager.nomin_fiat_market, 148 | "HavvenFiatOrderBook": lambda h: h.market_manager.havven_fiat_market, 149 | "HavvenNominOrderBook": lambda h: h.market_manager.havven_nomin_market 150 | } 151 | 152 | agent_reporters = {} 153 | for name in agents.player_names: 154 | if name not in agents.players_to_exclude: 155 | agent_reporters[name] = lambda h, y=name: round(mean_agent_profit_fraction(y, h)*100, 3) 156 | 157 | base_reporters.update(agent_reporters) 158 | 159 | return DataCollector( 160 | model_reporters=base_reporters, 161 | agent_reporters={ 162 | "Agents": lambda a: a, 163 | } 164 | ) 165 | -------------------------------------------------------------------------------- /experiments.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Imports and configuration\n", 10 | "from tqdm import tqdm\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "import seaborn as sns\n", 13 | "\n", 14 | "%load_ext line_profiler\n", 15 | "%config InlineBackend.figure_format = 'retina'\n", 16 | "sns.set_style(\"ticks\")\n", 17 | "plt.rc(\"axes.spines\", top=False, right=False)" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# Model Parameters\n", 27 | "from core import model\n", 28 | "from core import settingsloader\n", 29 | "\n", 30 | "experiment_settings = {\n", 31 | " \"Server\": {\n", 32 | " 'max_steps': 1500\n", 33 | " },\n", 34 | " \"Model\": {\n", 35 | " \"num_agents\": 100,\n", 36 | " \"continuous_order_matching\": True\n", 37 | " }\n", 38 | "}\n", 39 | "\n", 40 | "\n", 41 | "def run_sim(progress_bar=True):\n", 42 | " \"\"\"Run and return a havven simulation\"\"\"\n", 43 | " \n", 44 | " settings = settingsloader.load_settings()\n", 45 | " \n", 46 | " for category in experiment_settings:\n", 47 | " for setting in experiment_settings[category]:\n", 48 | " settings[category][setting] = experiment_settings[category][setting]\n", 49 | " \n", 50 | " model_settings = settings['Model']\n", 51 | " model_settings['agent_fractions'] = settings['AgentFractions']\n", 52 | "\n", 53 | " m = model.HavvenModel(\n", 54 | " model_settings,\n", 55 | " settings['Fees'],\n", 56 | " settings['Agents'],\n", 57 | " settings['Havven']\n", 58 | " )\n", 59 | " \n", 60 | " if progress_bar:\n", 61 | " for _ in tqdm(range(settings[\"Server\"][\"max_steps\"])):\n", 62 | " m.step()\n", 63 | " else:\n", 64 | " for _ in range(settings[\"Server\"][\"max_steps\"]):\n", 65 | " m.step()\n", 66 | " \n", 67 | " return m" 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": null, 73 | "metadata": {}, 74 | "outputs": [], 75 | "source": [ 76 | "# Run the model here\n", 77 | "havven = run_sim()" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": { 84 | "scrolled": false 85 | }, 86 | "outputs": [], 87 | "source": [ 88 | "# Collect and plot the data\n", 89 | "\n", 90 | "df = havven.datacollector.get_model_vars_dataframe()\n", 91 | "df[[\"Nomin Price\"]].plot()\n", 92 | "df[[\"Havven Price\"]].plot()\n", 93 | "df[[\"Havven/Nomin Price\"]].plot()\n", 94 | "df[[\"Havven Demand\", \"Havven Supply\"]].plot()\n", 95 | "df[[\"Nomin Demand\", \"Nomin Supply\"]].plot()\n", 96 | "df[[\"Fiat Demand\", \"Fiat Supply\"]].plot()\n", 97 | "df[[\"Avg Profit %\"]].plot()\n", 98 | "df[[\"Escrowed Havvens\", \"Nomins\"]].plot()\n", 99 | "df[[\"Fee Pool\", \"Fees Distributed\"]].plot()\n", 100 | "\n", 101 | "plt.show()" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": { 108 | "scrolled": true 109 | }, 110 | "outputs": [], 111 | "source": [ 112 | "# Profiling\n", 113 | "# import agents, orderbook\n", 114 | "\n", 115 | "# Overall sim\n", 116 | "# Result: 99.8% time spent in m.step()\n", 117 | "# %lprun -f run_sim run_sim(False)\n", 118 | "\n", 119 | "# Individual sim steps\n", 120 | "# Result: 83% in agent.step(); 17% in datacollector.collect()\n", 121 | "# %lprun -f model.Havven.step run_sim(False)\n", 122 | "\n", 123 | "# Per-agent sim steps\n", 124 | "# Result: ~40% each in agents.MarketPlayer.sell_fiat_for_havvens and sell_nomins_for_havvens,\n", 125 | "# 8% in OrderBook.Order.cancel\n", 126 | "# %lprun -f agents.Banker.step run_sim(False)\n", 127 | "\n", 128 | "# Market player trades\n", 129 | "# Result: 92% in OrderBook.buy, 6% in OrderBook.lowest_ask_price\n", 130 | "# %lprun -f agents.MarketPlayer.sell_fiat_for_havvens run_sim(False)\n", 131 | "# %lprun -f agents.MarketPlayer.sell_nomins_for_havven:s run_sim(False)\n", 132 | "\n", 133 | "# Order cancellations\n", 134 | "# Result: 74% in OrderBook.buy_orders.remove, 8% in Orderbook.step,\n", 135 | "# 6% in agents.MarketPlayer.notify_cancelled, 5% in agents.MarketPlayer.orders.remove\n", 136 | "# %lprun -f orderbook.Bid.cancel run_sim(False)\n", 137 | "# %lprun -f orderbook.Ask.cancel run_sim(False)\n", 138 | "\n", 139 | "# Orderbook buy and ask prices\n", 140 | "# Result: 93% orderbook.OrderBook.bid, 6% orderbook.OrderBook.lowest_ask_price\n", 141 | "# %lprun -f orderbook.OrderBook.buy run_sim(False)\n", 142 | "\n", 143 | "# Orderbook bid\n", 144 | "# Result: 79% orderbook.OrderBook.match, 20% construction of Bids\n", 145 | "# %lprun -f orderbook.OrderBook.bid run_sim(False)\n", 146 | "\n", 147 | "# Matching functions\n", 148 | "# Results: 24% match loop condition, 10% grabbing prev asks and bids from order lists\n", 149 | "# 32% matcher functions, 16% spread calculation, 15% recalculating prices.\n", 150 | "# %lprun -f orderbook.OrderBook.match run_sim(False)\n", 151 | "\n", 152 | "# Bid construction\n", 153 | "# Result: 22% superconstructor, 5% issuer order set addition,\n", 154 | "# 59% order book buy order addition, 9% book.step\n", 155 | "# %lprun -f orderbook.Bid.__init__ run_sim(False)" 156 | ] 157 | } 158 | ], 159 | "metadata": { 160 | "kernelspec": { 161 | "display_name": "Python 3", 162 | "language": "python", 163 | "name": "python3" 164 | }, 165 | "language_info": { 166 | "codemirror_mode": { 167 | "name": "ipython", 168 | "version": 3 169 | }, 170 | "file_extension": ".py", 171 | "mimetype": "text/x-python", 172 | "name": "python", 173 | "nbconvert_exporter": "python", 174 | "pygments_lexer": "ipython3", 175 | "version": "3.6.2" 176 | } 177 | }, 178 | "nbformat": 4, 179 | "nbformat_minor": 2 180 | } 181 | -------------------------------------------------------------------------------- /managers/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for holding onto values and functions for the model and agents 3 | """ 4 | 5 | from .havvenmanager import HavvenManager 6 | from .feemanager import FeeManager 7 | from .marketmanager import MarketManager 8 | from .agentmanager import AgentManager 9 | from .mint import Mint 10 | -------------------------------------------------------------------------------- /managers/agentmanager.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | from typing import Dict, List 3 | 4 | import agents as ag 5 | 6 | 7 | class AgentManager: 8 | """Manages agent populations.""" 9 | 10 | def __init__(self, 11 | havven_model: "model.HavvenModel", 12 | num_agents: int, 13 | agent_fractions: Dict[str, float], 14 | agent_settings: Dict[str, any]) -> None: 15 | """ 16 | :param havven_model: a reference to the sim itself. 17 | :param num_agents: the number of agents to include in this simulation (plus or minus a handful). 18 | :param agent_fractions: the vector which sets the proportions of each agent in the model. Will be normalised. 19 | :param agent_settings: dict holding values from setting file 20 | - init_value: the initial value from which to calculate agent endowments. 21 | - agent_minimum: the minimum number of each type of agent to include in the simulation. 1 by default. 22 | """ 23 | 24 | self.wealth_parameter = agent_settings['wealth_parameter'] 25 | self.agent_minimum = agent_settings['agent_minimum'] 26 | 27 | # A reference to the Havven sim itself. 28 | self.havven_model = havven_model 29 | 30 | # Lists of each type of agent. 31 | self.agents: Dict[str, List[ag.MarketPlayer]] = { 32 | name: [] for name in ag.player_names 33 | } 34 | self.agents["others"] = [] 35 | 36 | # Normalise the fractions of the population each agent occupies. 37 | total_value = sum(agent_fractions.values()) 38 | normalised_fractions = {} 39 | if total_value > 0: 40 | for name in ag.player_names: 41 | if name in agent_fractions: 42 | normalised_fractions[name] = agent_fractions[name]/total_value 43 | agent_fractions = normalised_fractions 44 | 45 | # Create the agents themselves. 46 | running_player_total = 0 47 | for agent_type in agent_fractions: 48 | total = max( 49 | self.agent_minimum, 50 | int(num_agents*agent_fractions[agent_type]) 51 | ) 52 | 53 | for i in range(total): 54 | agent = ag.player_names[agent_type](running_player_total, self.havven_model) 55 | agent.setup(self.wealth_parameter) 56 | self.havven_model.schedule.add(agent) 57 | self.agents[agent_type].append(agent) 58 | running_player_total += 1 59 | 60 | # Add a central stabilisation bank 61 | # self._add_central_bank(running_player_total, self.num_agents, self.wealth_parameter) 62 | 63 | # Now that each agent has its initial endowment, make them remember it. 64 | for agent in self.havven_model.schedule.agents: 65 | agent.reset_initial_wealth() 66 | 67 | def add(self, agent): 68 | self.havven_model.schedule.add(agent) 69 | for name, item in ag.player_names.items(): 70 | if type(agent) == item: 71 | self.agents[name].append(agent) 72 | return 73 | else: 74 | self.agents['others'].append(agent) 75 | 76 | def _add_central_bank(self, unique_id, num_agents, init_value): 77 | central_bank = ag.CentralBank( 78 | unique_id, self.havven_model, fiat=Dec(num_agents * init_value), 79 | nomin_target=Dec('1.0') 80 | ) 81 | self.havven_model.endow_havvens(central_bank, 82 | Dec(num_agents * init_value)) 83 | self.havven_model.schedule.add(central_bank) 84 | self.agents["others"].append(central_bank) 85 | -------------------------------------------------------------------------------- /managers/feemanager.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | from decimal import Decimal as Dec 3 | from random import shuffle 4 | 5 | import agents 6 | from .havvenmanager import HavvenManager 7 | 8 | 9 | class FeeManager: 10 | """ 11 | Handles fee calculation. 12 | """ 13 | 14 | def __init__(self, model_manager: HavvenManager, fee_settings: Dict[str, Any]) -> None: 15 | """ 16 | :param model_manager: a model_manager object 17 | :param fee_settings: The settings for fees: 18 | - fee_period: how often fees are paid out to havven holders 19 | - stable_nomin_fee_level: the fee rate for nomins 20 | - stable_havven_fee_level: the fee rate for havvens 21 | - stable_fiat_fee_level: the fee rate for fiat 22 | - stable_nomin_issuance_fee: the fee rate for nomin issuance 23 | - stable_nomin_redemption_fee: the fee rate for nomin redemption 24 | """ 25 | self.model_manager = model_manager 26 | 27 | # Fees are distributed at regular intervals 28 | self.fee_period: int = fee_settings['fee_period'] 29 | 30 | # Multiplicative transfer fee rates 31 | self.nomin_fee_rate = Dec(fee_settings['stable_nomin_fee_level']) 32 | self.havven_fee_rate = Dec(fee_settings['stable_havven_fee_level']) 33 | self.fiat_fee_rate = Dec(fee_settings['stable_fiat_fee_level']) 34 | 35 | # Multiplicative issuance fee rates 36 | self.issuance_fee_rate = Dec(fee_settings['stable_nomin_issuance_fee']) 37 | self.redemption_fee_rate = Dec(fee_settings['stable_nomin_redemption_fee']) 38 | 39 | self.fees_distributed = Dec(0) 40 | 41 | def transferred_fiat_received(self, quantity: Dec) -> Dec: 42 | """ 43 | Returns the fiat received by the recipient if a given quantity (with fee) 44 | is transferred. 45 | A user can only transfer less than their total balance when fees 46 | are taken into account. 47 | """ 48 | return HavvenManager.round_decimal(quantity / (Dec(1) + self.fiat_fee_rate)) 49 | 50 | def transferred_havvens_received(self, quantity: Dec) -> Dec: 51 | """ 52 | Returns the havvens received by the recipient if a given quantity (with fee) 53 | is transferred. 54 | A user can only transfer less than their total balance when fees 55 | are taken into account. 56 | """ 57 | return HavvenManager.round_decimal(quantity / (Dec(1) + self.havven_fee_rate)) 58 | 59 | def transferred_nomins_received(self, quantity: Dec) -> Dec: 60 | """ 61 | Returns the nomins received by the recipient if a given quantity (with fee) 62 | is transferred. 63 | A user can only transfer less than their total balance when fees 64 | are taken into account. 65 | """ 66 | return HavvenManager.round_decimal(quantity / (Dec(1) + self.nomin_fee_rate)) 67 | 68 | def transferred_fiat_fee(self, quantity: Dec) -> Dec: 69 | """ 70 | Return the fee charged for transferring a quantity of fiat. 71 | """ 72 | return HavvenManager.round_decimal(quantity * self.fiat_fee_rate) 73 | 74 | def transferred_havvens_fee(self, quantity: Dec) -> Dec: 75 | """ 76 | Return the fee charged for transferring a quantity of havvens. 77 | """ 78 | return HavvenManager.round_decimal(quantity * self.havven_fee_rate) 79 | 80 | def transferred_nomins_fee(self, quantity: Dec) -> Dec: 81 | """ 82 | Return the fee charged for transferring a quantity of nomins. 83 | """ 84 | return HavvenManager.round_decimal(quantity * self.nomin_fee_rate) 85 | 86 | def distribute_fees(self, schedule_agents: List["agents.MarketPlayer"]) -> None: 87 | """ 88 | Distribute currently held nomins to holders of havvens. 89 | """ 90 | # Different fee modes: 91 | # * distributed by held havvens 92 | # TODO: * distribute by escrowed havvens 93 | # TODO: * distribute by issued nomins 94 | # TODO: * distribute by motility 95 | 96 | # reward in random order in case there's 97 | # some ordering bias I'm missing. 98 | shuffled_agents = list(schedule_agents) 99 | shuffle(shuffled_agents) 100 | 101 | pre_nomins = self.model_manager.nomins 102 | supply = self.model_manager.nomin_supply 103 | for agent in shuffled_agents: 104 | if self.model_manager.nomins <= 0: 105 | break 106 | qty = min(HavvenManager.round_decimal(pre_nomins * agent.issued_nomins / supply), 107 | self.model_manager.nomins) 108 | agent.nomins += qty 109 | self.model_manager.nomins -= qty 110 | self.fees_distributed += qty 111 | -------------------------------------------------------------------------------- /managers/havvenmanager.py: -------------------------------------------------------------------------------- 1 | from decimal import getcontext, ROUND_HALF_UP 2 | from decimal import Decimal as Dec 3 | from typing import Dict, Any 4 | 5 | 6 | class HavvenManager: 7 | """ 8 | Class to hold the Havven model's variables 9 | """ 10 | 11 | currency_precision = 8 12 | """ 13 | Number of decimal places for currency precision. 14 | The decimal context precision should be significantly higher than this. 15 | """ 16 | 17 | def __init__(self, utilisation_ratio_max: Dec, 18 | continuous_order_matching: bool, havven_settings: Dict[str, Any]) -> None: 19 | """ 20 | :param utilisation_ratio_max: 21 | :param continuous_order_matching: 22 | :param havven_settings: 23 | - havven_supply: the total amount of havvens in the system 24 | - nomin_supply: the amount of nomins the havven system begins with 25 | - rolling_avg_time_window: the amount of steps to consider when calculating the 26 | rolling price average 27 | - use_volume_weighted_avg: whether to use volume in calculating the rolling price average 28 | """ 29 | # Set the decimal rounding mode 30 | getcontext().rounding = ROUND_HALF_UP 31 | 32 | # Initiate Time 33 | self.time: int = 0 34 | 35 | # Utilisation Ratio maximum (between 0 and 1) 36 | self.utilisation_ratio_max: Dec = utilisation_ratio_max 37 | 38 | # If true, match orders whenever an order is posted, 39 | # otherwise do so at the end of each period 40 | self.continuous_order_matching: bool = continuous_order_matching 41 | 42 | # Money Supply 43 | self.havven_supply = Dec(havven_settings['havven_supply']) 44 | self.nomin_supply = Dec(havven_settings['nomin_supply']) 45 | self.escrowed_havvens = Dec(0) 46 | 47 | # Havven's own capital supplies 48 | self.havvens: Dec = self.havven_supply 49 | self.nomins: Dec = self.nomin_supply 50 | self.fiat = Dec(0) 51 | 52 | self.rolling_avg_time_window: int = havven_settings['rolling_avg_time_window'] 53 | self.volume_weighted_average: bool = havven_settings['use_volume_weighted_avg'] 54 | """Whether to calculate the rolling average taking into account the volume of the trades""" 55 | 56 | @classmethod 57 | def round_float(cls, value: float) -> Dec: 58 | """ 59 | Round a float (as a Decimal) to the number of decimal places specified by 60 | the precision setting. 61 | Equivalent to Dec(value).quantize(Dec(1e(-cls.currency_precision))). 62 | """ 63 | # This check for numbers which are smaller than the precision allows will 64 | # be commented out for now as it seems to kill economic activity. 65 | # if value < 1E-8: 66 | # return Dec(0) 67 | return round(Dec(value), cls.currency_precision) 68 | 69 | @classmethod 70 | def round_decimal(cls, value: Dec) -> Dec: 71 | """ 72 | Round a Decimal to the number of decimal places specified by 73 | the precision setting. 74 | Equivalent to Dec(value).quantize(Dec(1e(-cls.currency_precision))). 75 | This function really only need be used for products and quotients. 76 | """ 77 | # This check for numbers which are smaller than the precision allows will 78 | # be commented out for now as it seems to kill economic activity. 79 | # if value < Dec('1E-8'): 80 | # return Dec(0) 81 | return round(value, cls.currency_precision) 82 | -------------------------------------------------------------------------------- /managers/marketmanager.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | from typing import Optional, Callable 3 | 4 | import agents as ag 5 | from core import orderbook as ob 6 | from .feemanager import FeeManager 7 | from .havvenmanager import HavvenManager 8 | 9 | 10 | class MarketManager: 11 | """ 12 | Handles all order books, trades, transfers, and conversions. 13 | """ 14 | 15 | def __init__(self, model_manager: HavvenManager, fee_manager: FeeManager) -> None: 16 | 17 | self.model_manager = model_manager 18 | self.fee_manager = fee_manager 19 | 20 | # Order books 21 | # If a book is X_Y_market, then X is the base currency, 22 | # Y is the quote currency. 23 | # That is, buyers hold Y and sellers hold X. 24 | self.havven_nomin_market = ob.OrderBook( 25 | model_manager, "havvens", "nomins", self.havven_nomin_match, 26 | self.fee_manager.transferred_nomins_fee, 27 | self.fee_manager.transferred_havvens_fee, 28 | self.fee_manager.transferred_nomins_received, 29 | self.fee_manager.transferred_havvens_received, 30 | self.model_manager.continuous_order_matching 31 | ) 32 | self.havven_fiat_market = ob.OrderBook( 33 | model_manager, "havvens", "fiat", self.havven_fiat_match, 34 | self.fee_manager.transferred_fiat_fee, 35 | self.fee_manager.transferred_havvens_fee, 36 | self.fee_manager.transferred_fiat_received, 37 | self.fee_manager.transferred_havvens_received, 38 | self.model_manager.continuous_order_matching 39 | ) 40 | self.nomin_fiat_market = ob.OrderBook( 41 | model_manager, "nomins", "fiat", self.nomin_fiat_match, 42 | self.fee_manager.transferred_fiat_fee, 43 | self.fee_manager.transferred_nomins_fee, 44 | self.fee_manager.transferred_fiat_received, 45 | self.fee_manager.transferred_nomins_received, 46 | self.model_manager.continuous_order_matching 47 | ) 48 | 49 | def __bid_ask_match( 50 | self, bid: "ob.Bid", ask: "ob.Ask", 51 | bid_success: Callable[["ag.MarketPlayer", Dec, Dec], bool], 52 | ask_success: Callable[["ag.MarketPlayer", Dec, Dec], bool], 53 | bid_transfer: Callable[["ag.MarketPlayer", "ag.MarketPlayer", Dec, Dec], bool], 54 | ask_transfer: Callable[["ag.MarketPlayer", "ag.MarketPlayer", Dec, Dec], bool] 55 | ) -> Optional["ob.TradeRecord"]: 56 | """ 57 | If possible, match the given bid and ask, with the given transfer 58 | and success functions. 59 | Cancel any orders which an agent cannot afford to service. 60 | Return a TradeRecord object if the match succeeded, otherwise None. 61 | """ 62 | 63 | if ask.price > bid.price: 64 | return None 65 | 66 | # Price will be favourable to whoever went second. 67 | # The earlier poster trades at their posted price, 68 | # while the later poster transacts at a price no worse than posted; 69 | # they may do better. 70 | price = ask.price if ask.time < bid.time else bid.price 71 | quantity = HavvenManager.round_decimal(min(ask.quantity, bid.quantity)) 72 | 73 | # Only charge a fraction of the fee if an order was not entirely filled. 74 | bid_fee = HavvenManager.round_decimal((quantity/bid.quantity) * bid.fee) 75 | ask_fee = HavvenManager.round_decimal((quantity/ask.quantity) * ask.fee) 76 | 77 | # Compute the buy value. The sell value is just the quantity itself. 78 | buy_val = HavvenManager.round_decimal(quantity * price) 79 | 80 | # Only perform the actual transfer if it would be successful. 81 | # Cancel any orders that would not succeed. 82 | fail = False 83 | if not bid_success(bid.issuer, buy_val, bid_fee): 84 | bid.cancel() 85 | fail = True 86 | if not ask_success(ask.issuer, quantity, ask_fee): 87 | ask.cancel() 88 | fail = True 89 | if fail: 90 | return None 91 | # Perform the actual transfers. 92 | # We have already checked above if these would succeed. 93 | bid_transfer(bid.issuer, ask.issuer, buy_val, bid_fee) 94 | ask_transfer(ask.issuer, bid.issuer, quantity, ask_fee) 95 | 96 | # Update the orders, cancelling any with 0 remaining quantity. 97 | # This will remove the amount that was transferred from issuers' used value. 98 | bid.update_quantity(bid.quantity - quantity, bid.fee - bid_fee) 99 | ask.update_quantity(ask.quantity - quantity, ask.fee - ask_fee) 100 | return ob.TradeRecord(bid.issuer, ask.issuer, ask.book, 101 | price, quantity, bid_fee, ask_fee, self.model_manager.time) 102 | 103 | def havven_nomin_match(self, bid: "ob.Bid", 104 | ask: "ob.Ask") -> Optional["ob.TradeRecord"]: 105 | """ 106 | Buyer offers nomins in exchange for havvens from the seller. 107 | Return a TradeRecord object if the match succeeded, otherwise None. 108 | """ 109 | return self.__bid_ask_match(bid, ask, 110 | self.transfer_nomins_success, 111 | self.transfer_havvens_success, 112 | self.transfer_nomins, 113 | self.transfer_havvens) 114 | 115 | def havven_fiat_match(self, bid: "ob.Bid", 116 | ask: "ob.Ask") -> Optional["ob.TradeRecord"]: 117 | """ 118 | Buyer offers fiat in exchange for havvens from the seller. 119 | Return a TradeRecord object if the match succeeded, otherwise None. 120 | """ 121 | return self.__bid_ask_match(bid, ask, 122 | self.transfer_fiat_success, 123 | self.transfer_havvens_success, 124 | self.transfer_fiat, 125 | self.transfer_havvens) 126 | 127 | def nomin_fiat_match(self, bid: "ob.Bid", 128 | ask: "ob.Ask") -> Optional["ob.TradeRecord"]: 129 | """ 130 | Buyer offers fiat in exchange for nomins from the seller. 131 | Return a TradeRecord object if the match succeeded, otherwise None. 132 | """ 133 | return self.__bid_ask_match(bid, ask, 134 | self.transfer_fiat_success, 135 | self.transfer_nomins_success, 136 | self.transfer_fiat, 137 | self.transfer_nomins) 138 | 139 | def transfer_fiat_success(self, sender: "ag.MarketPlayer", 140 | quantity: Dec, fee: Dec) -> bool: 141 | """True iff the sender could successfully send a quantity of fiat.""" 142 | return 0 <= quantity + fee <= HavvenManager.round_decimal(sender.fiat) 143 | 144 | def transfer_havvens_success(self, sender: "ag.MarketPlayer", 145 | quantity: Dec, fee: Dec) -> bool: 146 | """True iff the sender could successfully send a quantity of havvens.""" 147 | return 0 <= quantity + fee <= HavvenManager.round_decimal(sender.havvens) 148 | 149 | def transfer_nomins_success(self, sender: "ag.MarketPlayer", 150 | quantity: Dec, fee: Dec) -> bool: 151 | """True iff the sender could successfully send a quantity of nomins.""" 152 | return 0 <= quantity + fee <= HavvenManager.round_decimal(sender.nomins) 153 | 154 | def transfer_fiat(self, sender: "ag.MarketPlayer", 155 | recipient: "ag.MarketPlayer", quantity: Dec, fee: Optional[Dec] = None) -> bool: 156 | """ 157 | Transfer a positive quantity of fiat currency from the sender to the 158 | recipient, if balance is sufficient. Return True on success. 159 | """ 160 | if fee is None: 161 | fee = self.fee_manager.transferred_fiat_fee(quantity) 162 | if self.transfer_fiat_success(sender, quantity, fee): 163 | sender.fiat -= quantity + fee 164 | recipient.fiat += quantity 165 | self.model_manager.fiat += fee 166 | return True 167 | return False 168 | 169 | def transfer_havvens(self, sender: 'ag.MarketPlayer', 170 | recipient: 'ag.MarketPlayer', quantity: Dec, fee: Optional[Dec] = None) -> bool: 171 | """ 172 | Transfer a positive quantity of havvens from the sender to the recipient, 173 | if balance is sufficient. Return True on success. 174 | """ 175 | if fee is None: 176 | fee = self.fee_manager.transferred_havvens_fee(quantity) 177 | if self.transfer_havvens_success(sender, quantity, fee): 178 | sender.havvens -= quantity + fee 179 | recipient.havvens += quantity 180 | self.model_manager.havvens += fee 181 | return True 182 | return False 183 | 184 | def transfer_nomins(self, sender: 'ag.MarketPlayer', 185 | recipient: 'ag.MarketPlayer', quantity: Dec, fee: Optional[Dec] = None) -> bool: 186 | """ 187 | Transfer a positive quantity of nomins from the sender to the recipient, 188 | if balance is sufficient. Return True on success. 189 | """ 190 | if fee is None: 191 | fee = self.fee_manager.transferred_nomins_fee(quantity) 192 | if self.transfer_nomins_success(sender, quantity, fee): 193 | sender.nomins -= quantity + fee 194 | recipient.nomins += quantity 195 | self.model_manager.nomins += fee 196 | return True 197 | return False 198 | 199 | def havvens_to_nomins(self, quantity: Dec) -> Dec: 200 | """Convert a quantity of havvens to its equivalent quantity in nomins.""" 201 | return HavvenManager.round_decimal(quantity * self.havven_nomin_market.price) 202 | 203 | def havvens_to_fiat(self, quantity: Dec) -> Dec: 204 | """Convert a quantity of havvens to its equivalent quantity in fiat.""" 205 | return HavvenManager.round_decimal(quantity * self.havven_fiat_market.price) 206 | 207 | def nomins_to_havvens(self, quantity: Dec) -> Dec: 208 | """Convert a quantity of nomins to its equivalent quantity in havvens.""" 209 | return HavvenManager.round_decimal(quantity / self.havven_nomin_market.price) 210 | 211 | def nomins_to_fiat(self, quantity: Dec) -> Dec: 212 | """Convert a quantity of nomins to its equivalent quantity in fiat.""" 213 | return HavvenManager.round_decimal(quantity * self.nomin_fiat_market.price) 214 | 215 | def fiat_to_havvens(self, quantity: Dec) -> Dec: 216 | """Convert a quantity of fiat to its equivalent quantity in havvens.""" 217 | return HavvenManager.round_decimal(quantity / self.havven_fiat_market.price) 218 | 219 | def fiat_to_nomins(self, quantity: Dec) -> Dec: 220 | """Convert a quantity of fiat to its equivalent quantity in nomins.""" 221 | return HavvenManager.round_decimal(quantity / self.nomin_fiat_market.price) 222 | -------------------------------------------------------------------------------- /managers/mint.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | 3 | import agents 4 | 5 | from .havvenmanager import HavvenManager 6 | from .marketmanager import MarketManager 7 | 8 | 9 | class Mint: 10 | """ 11 | Handles issuance and burning of nomins. 12 | """ 13 | 14 | def __init__(self, havven_manager: HavvenManager, 15 | market_manager: MarketManager) -> None: 16 | self.havven_manager = havven_manager 17 | self.market_manager = market_manager 18 | 19 | def escrow_havvens(self, agent: "agents.MarketPlayer", 20 | value: Dec) -> bool: 21 | """ 22 | Escrow a positive value of havvens in order to be able to issue 23 | nomins against them. 24 | """ 25 | if agent.available_havvens >= value >= 0: 26 | agent.havvens -= value 27 | agent.escrowed_havvens += value 28 | self.havven_manager.escrowed_havvens += value 29 | return True 30 | return False 31 | 32 | def unescrow_havvens(self, agent: "agents.MarketPlayer", 33 | value: Dec) -> bool: 34 | """ 35 | Unescrow a quantity of havvens, if there are not too many 36 | issued nomins locking it. 37 | """ 38 | if 0 <= value <= self.available_escrowed_havvens(agent): 39 | agent.havvens += value 40 | agent.escrowed_havvens -= value 41 | self.havven_manager.escrowed_havvens -= value 42 | return True 43 | return False 44 | 45 | ### FIXME TODO ### 46 | ### THIS LOGIC IS BROKEN. UTILISATION RATIO NOT TAKEN INTO ACCOUNT AT EVERY LOCATION ### 47 | ### ALSO NEED TO ENSURE THAT NOMINS ARE ACTUALLY PROPERLY-ISSUABLE ### 48 | 49 | def available_escrowed_havvens(self, agent: "agents.MarketPlayer") -> Dec: 50 | """ 51 | Return the quantity of escrowed havvens which is not 52 | locked by issued nomins. May be negative. 53 | """ 54 | return agent.escrowed_havvens - self.unavailable_escrowed_havvens(agent) 55 | 56 | def unavailable_escrowed_havvens(self, agent: "agents.MarketPlayer") -> Dec: 57 | """ 58 | Return the quantity of locked escrowed havvens, 59 | having had nomins issued against it. 60 | May be greater than total escrowed havvens. 61 | """ 62 | return self.market_manager.nomins_to_havvens(agent.issued_nomins) 63 | 64 | def max_issuance_rights(self, agent: "agents.MarketPlayer") -> Dec: 65 | """ 66 | The total quantity of nomins this agent has a right to issue. 67 | """ 68 | return HavvenManager.round_decimal(self.market_manager.havvens_to_nomins(agent.escrowed_havvens) * \ 69 | self.havven_manager.utilisation_ratio_max) 70 | 71 | def remaining_issuance_rights(self, agent: "agents.MarketPlayer") -> Dec: 72 | """ 73 | Return the remaining quantity of tokens this agent can issued on the back of their 74 | escrowed havvens. May be negative. 75 | """ 76 | return self.max_issuance_rights(agent) - agent.issued_nomins 77 | 78 | def issue_nomins(self, agent: "agents.MarketPlayer", value: Dec) -> bool: 79 | """ 80 | Issue a positive value of nomins against currently escrowed havvens, 81 | up to the utilisation ratio maximum. 82 | """ 83 | remaining = self.remaining_issuance_rights(agent) 84 | if 0 <= value <= remaining: 85 | agent.issued_nomins += value 86 | agent.nomins += value 87 | self.havven_manager.nomin_supply += value 88 | return True 89 | return False 90 | 91 | def burn_nomins(self, agent: "agents.MarketPlayer", value: Dec) -> bool: 92 | """ 93 | Burn a positive value of issued nomins, which frees up havvens. 94 | """ 95 | if 0 <= value <= agent.available_nomins and value <= agent.issued_nomins: 96 | agent.nomins -= value 97 | agent.issued_nomins -= value 98 | self.havven_manager.nomin_supply -= value 99 | return True 100 | return False 101 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = test test/test_agents test/test_managers -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | jupyter 2 | line-profiler 3 | psutil 4 | matplotlib 5 | seaborn 6 | mesa 7 | numpy 8 | scipy 9 | sortedcontainers 10 | tqdm 11 | pytest -------------------------------------------------------------------------------- /reset.py: -------------------------------------------------------------------------------- 1 | from core import settingsloader 2 | from core import cache_handler 3 | import os 4 | 5 | 6 | if __name__ == "__main__": 7 | x = input("Clear and refresh settings.ini (y/[any])? ") 8 | if x.lower() in ['y', 'yes']: 9 | # clear current settings 10 | os.remove("settings.ini") 11 | settings = settingsloader.load_settings() 12 | 13 | x = input("Clear and refresh cache_data.pkl (y/[any])? ") 14 | if x.lower() in ['y', 'yes']: 15 | data = cache_handler.generate_new_caches({}) 16 | cache_handler.save_data(data) 17 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | """run.py: main entrypoint for the Havven simulation.""" 2 | from mesa.visualization.ModularVisualization import ModularServer 3 | 4 | from core import server 5 | 6 | S: ModularServer = server.make_server() 7 | S.launch() 8 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/__init__.py -------------------------------------------------------------------------------- /test/test_agents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/__init__.py -------------------------------------------------------------------------------- /test/test_agents/test_arbitrageur.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/test_arbitrageur.py -------------------------------------------------------------------------------- /test/test_agents/test_banker.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/test_banker.py -------------------------------------------------------------------------------- /test/test_agents/test_centralbank.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/test_centralbank.py -------------------------------------------------------------------------------- /test/test_agents/test_marketplayer.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/test_marketplayer.py -------------------------------------------------------------------------------- /test/test_agents/test_nominshorter.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/test_nominshorter.py -------------------------------------------------------------------------------- /test/test_agents/test_randomizer.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_agents/test_randomizer.py -------------------------------------------------------------------------------- /test/test_managers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_managers/__init__.py -------------------------------------------------------------------------------- /test/test_managers/test_agentmanager.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_managers/test_agentmanager.py -------------------------------------------------------------------------------- /test/test_managers/test_feemanager.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_managers/test_feemanager.py -------------------------------------------------------------------------------- /test/test_managers/test_havvenmanager.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_managers/test_havvenmanager.py -------------------------------------------------------------------------------- /test/test_managers/test_marketmanager.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_managers/test_marketmanager.py -------------------------------------------------------------------------------- /test/test_managers/test_mint.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_managers/test_mint.py -------------------------------------------------------------------------------- /test/test_model.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | 3 | from core import model 4 | 5 | 6 | def test_fiat_value(): 7 | havven_model = model.HavvenModel(10) 8 | assert(isinstance(havven_model.fiat_value(), Dec)) 9 | assert(havven_model.fiat_value() == Dec(0)) 10 | assert(havven_model.fiat_value(Dec(1), Dec(1), Dec(1)) > Dec(0)) 11 | assert(havven_model.fiat_value(havvens=Dec(0), 12 | nomins=Dec(0), 13 | fiat=Dec(1)) == Dec(1)) 14 | assert(havven_model.fiat_value(havvens=Dec(1)) < 15 | havven_model.fiat_value(havvens=Dec(2))) 16 | assert(havven_model.fiat_value(nomins=Dec(1)) < 17 | havven_model.fiat_value(nomins=Dec(2))) 18 | assert(havven_model.fiat_value(fiat=Dec(1)) < 19 | havven_model.fiat_value(fiat=Dec(2))) 20 | 21 | 22 | def test_endowment(): 23 | havven_model = model.HavvenModel(10) 24 | agent = havven_model.schedule.agents[0] 25 | agent_pre_cur = agent.havvens 26 | havven_pre_cur = havven_model.manager.havvens 27 | 28 | havven_model.endow_havvens(agent, Dec(0)) 29 | havven_model.endow_havvens(agent, Dec(-10)) 30 | assert(agent.havvens == agent_pre_cur) 31 | assert(havven_model.manager.havvens == havven_pre_cur) 32 | 33 | endowment = Dec(100) 34 | havven_model.endow_havvens(agent, endowment) 35 | assert(agent.havvens == agent_pre_cur + endowment) 36 | assert(havven_model.manager.havvens == havven_pre_cur - endowment) 37 | 38 | 39 | def test_step(): 40 | havven_model = model.HavvenModel(20) 41 | assert(havven_model.manager.time == 1) 42 | havven_model.step() 43 | assert(havven_model.manager.time == 2) 44 | 45 | time_delta = 100 46 | for _ in range(time_delta): 47 | havven_model.step() 48 | assert(havven_model.manager.time == time_delta + 2) 49 | 50 | 51 | def test_fee_distribution_period(): 52 | havven_model = model.HavvenModel(20) 53 | assert(havven_model.fee_manager.fees_distributed == Dec(0)) 54 | assert(havven_model.manager.nomins == Dec(0)) 55 | 56 | for _ in range(havven_model.fee_manager.fee_period - 1): 57 | havven_model.step() 58 | 59 | prenomins = havven_model.manager.nomins 60 | predistrib = havven_model.fee_manager.fees_distributed 61 | assert(prenomins > Dec(0)) 62 | assert(predistrib == Dec(0)) 63 | 64 | havven_model.step() 65 | 66 | postnomins = havven_model.manager.nomins 67 | postdistrib = havven_model.fee_manager.fees_distributed 68 | assert(postnomins == Dec(0)) 69 | assert(prenomins <= postdistrib) 70 | assert(havven_model.manager.nomins == Dec(0)) 71 | 72 | -------------------------------------------------------------------------------- /test/test_server.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_server.py -------------------------------------------------------------------------------- /test/test_stats.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/test/test_stats.py -------------------------------------------------------------------------------- /visualization/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Mesa Visualization Module 4 | ------------------------- 5 | 6 | TextVisualization: Base class for writing ASCII visualizations of model state. 7 | 8 | TextServer: Class which takes a TextVisualization child class as an input, and 9 | renders it in-browser, along with an interface. 10 | 11 | """ 12 | -------------------------------------------------------------------------------- /visualization/cached_server.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import threading 4 | import time 5 | 6 | import tornado.autoreload 7 | import tornado.escape 8 | import tornado.gen 9 | import tornado.ioloop 10 | import tornado.web 11 | import tornado.websocket 12 | 13 | from core import cache_handler 14 | 15 | 16 | class CachedPageHandler(tornado.web.RequestHandler): 17 | """ Handler for the HTML template which holds the visualization. """ 18 | def get(self): 19 | elements = self.application.visualization_elements 20 | for i, element in enumerate(elements): 21 | element.index = i 22 | self.render("cache_template.html", port=self.application.port, 23 | model_name=self.application.model_name, 24 | description=self.application.description, 25 | package_includes=self.application.package_includes, 26 | local_includes=self.application.local_includes, 27 | scripts=self.application.js_code, 28 | fps_max=self.application.fps_max, 29 | fps_default=self.application.fps_default) 30 | 31 | 32 | class CachedSocketHandler(tornado.websocket.WebSocketHandler): 33 | """ Handler for websocket. """ 34 | def __init__(self, *args, **kwargs): 35 | super().__init__(*args, **kwargs) 36 | self.resetlock = threading.Lock() 37 | self.step = 0 38 | self.last_step_time = time.time() 39 | 40 | def open(self): 41 | """ 42 | When a new user connects to the server via websocket create a new model 43 | i.e. the same IP can have multiple models. 44 | """ 45 | if self.application.verbose: 46 | print("Socket connection opened") 47 | 48 | def on_message(self, message): 49 | """ 50 | Receiving a message from the websocket, parse, and act accordingly. 51 | """ 52 | if self.application.verbose: 53 | print(message) 54 | msg = tornado.escape.json_decode(message) 55 | 56 | if msg["type"] == "get_steps": 57 | cache_data = self.application.cached_data_handler.get_step(msg['dataset'], msg['step']) 58 | if cache_data is False: 59 | message = {"type": "end"} 60 | else: 61 | data = [(msg['step']+1, cache_data)] 62 | 63 | message = { 64 | "type": "viz_state", 65 | "data": data, 66 | } 67 | self.write_message(message) 68 | elif msg["type"] == "get_datasets": 69 | data = self.application.cached_data_handler.get_dataset_info() 70 | message = { 71 | "type": "dataset_info", 72 | "data": data 73 | } 74 | self.write_message(message) 75 | else: 76 | if self.application.verbose: 77 | print("Unexpected message!") 78 | 79 | def on_close(self): 80 | """When the user closes the connection destroy the model""" 81 | if self.application.verbose: 82 | print("Connection closed:", self) 83 | del self 84 | 85 | 86 | class CachedDataHandler: 87 | def __init__(self, default_settings): 88 | self.default_settings = default_settings 89 | data = cache_handler.load_saved() 90 | all_cached = False 91 | for i in cache_handler.run_settings: 92 | if i['name'] not in data: 93 | break 94 | else: 95 | all_cached = True 96 | 97 | if not all_cached: 98 | data = cache_handler.generate_new_caches(data) 99 | cache_handler.save_data(data) 100 | 101 | self.data = data 102 | 103 | def get_steps(self, dataset, step_start, step_end): 104 | if dataset in self.data and \ 105 | 0 <= step_start < step_end < len(self.data[dataset]): 106 | return self.data[dataset]['data'][step_start:step_end] 107 | return False 108 | 109 | def get_step(self, dataset, step): 110 | if dataset in self.data and 0 <= step < len(self.data[dataset]['data']): 111 | return self.data[dataset]['data'][step] 112 | return False 113 | 114 | def get_dataset_info(self): 115 | to_send = [] 116 | for name in self.data: 117 | i = self.data[name] 118 | settings = copy.deepcopy(self.default_settings) 119 | for section in i["settings"]: 120 | if section not in settings: 121 | continue 122 | for item in i['settings'][section]: 123 | if item in settings[section]: 124 | settings[section][item] = i["settings"][section][item] 125 | to_send.append( 126 | { 127 | "name": name, 128 | "settings": settings, 129 | "max_steps": i["max_steps"], 130 | "description": i["description"] 131 | } 132 | ) 133 | return to_send 134 | 135 | 136 | class CachedModularServer(tornado.web.Application): 137 | """ Main visualization application. """ 138 | verbose = True 139 | 140 | port = 3000 # Default port to listen on 141 | 142 | # Handlers and other globals: 143 | page_handler = (r'/', CachedPageHandler) 144 | socket_handler = (r'/ws', CachedSocketHandler) 145 | static_handler = (r'/static/(.*)', tornado.web.StaticFileHandler, 146 | {"path": os.path.dirname(__file__) + "/templates"}) 147 | local_handler = (r'/local/(.*)', tornado.web.StaticFileHandler, 148 | {"path": ''}) 149 | 150 | handlers = [page_handler, socket_handler, static_handler, local_handler] 151 | 152 | settings = {"debug": True, 153 | "autoreload": False, 154 | "template_path": os.path.dirname(__file__) + "/templates"} 155 | 156 | EXCLUDE_LIST = ('width', 'height',) 157 | 158 | def __init__(self, settings, visualization_elements, name): 159 | self.port = settings['Server']['port'] 160 | 161 | # Prep visualization elements: 162 | self.cached = settings['Server']['cached'] 163 | if self.cached: 164 | self.cached_data_handler = CachedDataHandler(settings) 165 | self.threaded = settings['Server']['threaded'] 166 | self.visualization_elements = visualization_elements 167 | self.model_name = name 168 | self.fps_max = settings['Server']['fps_max'] 169 | self.fps_default = settings['Server']['fps_default'] 170 | 171 | self.description = "" 172 | 173 | self.visualization_elements = visualization_elements 174 | self.package_includes = set() 175 | self.local_includes = set() 176 | self.js_code = [] 177 | for element in self.visualization_elements: 178 | for include_file in element.package_includes: 179 | self.package_includes.add(include_file) 180 | for include_file in element.local_includes: 181 | self.local_includes.add(include_file) 182 | self.js_code.append(element.js_code) 183 | 184 | # Initializing the application itself: 185 | super().__init__(self.handlers, **self.settings) 186 | 187 | def launch(self, port=None): 188 | """ Run the app. """ 189 | startLoop = not tornado.ioloop.IOLoop.initialized() 190 | if port is not None: 191 | self.port = port 192 | url = 'http://127.0.0.1:{PORT}'.format(PORT=self.port) 193 | print('Interface starting at {url}'.format(url=url)) 194 | self.listen(self.port) 195 | tornado.autoreload.start() 196 | if startLoop: 197 | tornado.ioloop.IOLoop.instance().start() 198 | -------------------------------------------------------------------------------- /visualization/modules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Container for all built-in visualization modules. 4 | """ 5 | 6 | from visualization.modules.chart_visualization import ChartModule 7 | from visualization.modules.text_visualization import TextElement 8 | from visualization.modules.bargraph import BarGraphModule 9 | from visualization.modules.wealth_graphs import WealthModule, PortfolioModule, CurrentOrderModule, PastOrdersModule 10 | from visualization.modules.orderbook_depth import OrderBookModule 11 | from visualization.modules.candlestick import CandleStickModule 12 | -------------------------------------------------------------------------------- /visualization/modules/bargraph.py: -------------------------------------------------------------------------------- 1 | """bargraph.py: a module for rendering a histogram-style bar chart.""" 2 | 3 | from typing import List, Tuple, Dict 4 | 5 | from mesa.datacollection import DataCollector 6 | 7 | from core.model import HavvenModel 8 | from visualization.visualization_element import VisualizationElement 9 | 10 | 11 | class BarGraphModule(VisualizationElement): 12 | """ 13 | Displays a simple bar graph of the selected attributes of the agents 14 | """ 15 | package_includes: List[str] = ["BarGraphModule.js"] 16 | local_includes: List[str] = [] 17 | 18 | def __init__(self, series: List[Dict[str, str]], height: int = 150, 19 | width: int = 500, data_collector_name: str = "datacollector", 20 | title: str = "", desc: str = "", group: str = "") -> None: 21 | self.series = series 22 | self.height = height 23 | # currently width does nothing, as it stretches the whole page 24 | self.width = width 25 | self.data_collector_name = data_collector_name 26 | 27 | self.sent_data = False 28 | 29 | # the code to be rendered on the page, last bool is whether it will be a stack graph 30 | self.js_code: str = f"""elements.push(new BarGraphModule("{group}", "{title}", "{desc}", 31 | "{series[0]['Label']}",{width},{height}));""" 32 | 33 | def render(self, model: HavvenModel) -> List[Tuple[str, float]]: 34 | """ 35 | return the data to be sent to the websocket to be rendered on the page 36 | """ 37 | data_collector: "DataCollector" = getattr( 38 | model, self.data_collector_name 39 | ) 40 | vals: List[Tuple[str, float]] = [] 41 | 42 | return vals 43 | -------------------------------------------------------------------------------- /visualization/modules/candlestick.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | from typing import List, Tuple, Dict 3 | 4 | from mesa.datacollection import DataCollector 5 | 6 | from core import orderbook as ob 7 | from core.model import HavvenModel 8 | from visualization.visualization_element import VisualizationElement 9 | 10 | 11 | class CandleStickModule(VisualizationElement): 12 | """ 13 | Display a depth graph for order books to show the quantity 14 | of buy/sell orders for the given market 15 | """ 16 | package_includes: List[str] = ["CandleStickModule.js"] 17 | local_includes: List[str] = [] 18 | 19 | def __init__( 20 | self, series: List[Dict[str, str]], height: int = 150, 21 | width: int = 500, data_collector_name: str = "datacollector", 22 | desc: str = "", title: str = "", group: str = "") -> None: 23 | 24 | self.series = series 25 | self.height = height 26 | # currently width does nothing, as it stretches the whole page 27 | self.width = width 28 | self.data_collector_name = data_collector_name 29 | 30 | self.js_code = f"""elements.push( 31 | new CandleStickModule("{group}", "{title}", "{desc}", 32 | "{series[0]['Label']}",{width},{height}, 33 | "{series[0]['AvgColor']}","{series[0]['VolumeColor']}" 34 | ) 35 | );""" 36 | 37 | def render(self, model: HavvenModel) -> Tuple[Tuple[float, float, float, float], float, float]: 38 | """ 39 | return the data to be sent to the websocket to be rendered on the page 40 | in the format of [[candle data (hi,lo,open,close)], rolling price, volume] 41 | """ 42 | data_collector: "DataCollector" = getattr( 43 | model, self.data_collector_name 44 | ) 45 | price_data: List[Dec] = [] 46 | candle_data: List[Dec] = [] 47 | vol_data: List[Dec] = [] 48 | 49 | for s in self.series: # TODO: not use series, as it should only really be one graph 50 | name: str = s['orderbook'] 51 | 52 | # get the buy and sell orders of the named market and add together 53 | # the quantities or orders with the same rates 54 | 55 | try: 56 | order_book: "ob.OrderBook" = data_collector.model_vars[name][-1] 57 | candle_data = order_book.candle_data[:-1] 58 | price_data = order_book.price_data[1:] 59 | vol_data = order_book.volume_data[1:] 60 | except Exception: 61 | return (1., 1., 1., 1.), 1., 1. 62 | # convert decimals to floats 63 | return ( 64 | ( 65 | float(candle_data[-1][0]), 66 | float(candle_data[-1][1]), 67 | float(candle_data[-1][2]), 68 | float(candle_data[-1][3]) 69 | ), 70 | float(price_data[-1]), 71 | float(vol_data[-1]) 72 | ) 73 | -------------------------------------------------------------------------------- /visualization/modules/chart_visualization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Chart Module 4 | ============ 5 | 6 | Module for drawing live-updating line charts using Charts.js 7 | 8 | """ 9 | import json 10 | from visualization.visualization_element import VisualizationElement 11 | 12 | 13 | class ChartModule(VisualizationElement): 14 | """ Each chart can visualize one or more model-level series as lines 15 | with the data value on the Y axis and the step number as the X axis. 16 | 17 | At the moment, each call to the render method returns a list of the most 18 | recent values of each series. 19 | 20 | Attributes: 21 | series: A list of dictionaries containing information on series to 22 | plot. Each dictionary must contain (at least) the "Label" and 23 | "Color" keys. The "Label" value must correspond to a 24 | model-level series collected by the model's DataCollector, and 25 | "Color" must have a valid HTML color. 26 | canvas_height, canvas_width: The width and height to draw the chart on 27 | the page, in pixels. Default to 200 x 500 28 | data_collector_name: Name of the DataCollector object in the model to 29 | retrieve data from. 30 | 31 | Example: 32 | schelling_chart = ChartModule([{"Label": "happy", "Color": "Black"}], 33 | data_collector_name="datacollector") 34 | 35 | TODO: 36 | Have it be able to handle agent-level variables as well. 37 | 38 | More Pythonic customization; in particular, have both series-level and 39 | chart-level options settable in Python, and passed to the front-end 40 | the same way that "Color" is currently. 41 | 42 | """ 43 | package_includes = ["ChartModule.js"] 44 | 45 | def __init__(self, series, canvas_height=150, canvas_width=500, 46 | data_collector_name="datacollector", desc: str = "", 47 | title: str = "", group: str = ""): 48 | """ 49 | Create a new line chart visualization. 50 | 51 | Args: 52 | series: A list of dictionaries containing series names and 53 | HTML colors to chart them in, e.g. 54 | [{"Label": "happy", "Color": "Black"},] 55 | canvas_height, canvas_width: Size in pixels of the chart to draw. 56 | data_collector_name: Name of the DataCollector to use. 57 | """ 58 | 59 | self.series = series 60 | self.canvas_height = canvas_height 61 | self.canvas_width = canvas_width 62 | self.data_collector_name = data_collector_name 63 | 64 | series_json = json.dumps(self.series) 65 | self.js_code = f"""elements.push( 66 | new ChartModule("{group}", "{title}", "{desc}", {series_json}, 67 | {canvas_width}, {canvas_height}));""" 68 | 69 | def render(self, model): 70 | current_values = [] 71 | data_collector = getattr(model, self.data_collector_name) 72 | 73 | for s in self.series: 74 | name = s["Label"] 75 | try: 76 | val = data_collector.model_vars[name][-1] # Latest value 77 | except: 78 | val = 0 79 | current_values.append(val) 80 | return current_values 81 | -------------------------------------------------------------------------------- /visualization/modules/orderbook_depth.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as Dec 2 | from typing import List, Tuple, Dict 3 | 4 | from mesa.datacollection import DataCollector 5 | 6 | from core import orderbook as ob 7 | from core.model import HavvenModel 8 | from visualization.visualization_element import VisualizationElement 9 | 10 | 11 | class OrderBookModule(VisualizationElement): 12 | """ 13 | Display a depth graph for order books to show the quantity 14 | of buy/sell orders for the given market 15 | """ 16 | package_includes: List[str] = ["DepthGraphModule.js"] 17 | local_includes: List[str] = [] 18 | 19 | def __init__( 20 | self, series: List[Dict[str, str]], height: int = 150, 21 | width: int = 500, data_collector_name: str = "datacollector", 22 | desc: str = "", title: str = "", group: str = "") -> None: 23 | 24 | self.series = series 25 | self.height = height 26 | # currently width does nothing, as it stretches the whole page 27 | self.width = width 28 | self.data_collector_name = data_collector_name 29 | 30 | self.js_code = f"""elements.push( 31 | new DepthGraphModule("{group}", "{title}", "{desc}", "{series[0]['Label']}",{width},{height}) 32 | );""" 33 | 34 | def render(self, model: HavvenModel) -> List[List[Tuple[float, float]]]: 35 | """ 36 | return the data to be sent to the websocket to be rendered on the page 37 | """ 38 | data_collector: "DataCollector" = getattr( 39 | model, self.data_collector_name 40 | ) 41 | price = 1.0 42 | bids: List[Tuple[Dec, Dec]] = [] 43 | asks: List[Tuple[Dec, Dec]] = [] 44 | 45 | for s in self.series: # TODO: not use series, as it should only really be one graph 46 | name: str = s['Label'] 47 | 48 | # get the buy and sell orders of the named market and add together 49 | # the quantities or orders with the same rates 50 | 51 | try: 52 | order_book: "ob.OrderBook" = data_collector.model_vars[name][-1] 53 | price = order_book.price 54 | bids = order_book.bid_price_buckets.items() 55 | asks = order_book.ask_price_buckets.items() 56 | except Exception: 57 | bids = [] 58 | asks = [] 59 | 60 | # convert decimals to floats 61 | 62 | return [float(price), [(float(i[0]), float(i[1])) for i in bids], [(float(i[0]), float(i[1])) for i in asks]] 63 | -------------------------------------------------------------------------------- /visualization/modules/text_visualization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Text Module 4 | ============ 5 | 6 | Module for drawing live-updating text. 7 | 8 | """ 9 | from visualization.visualization_element import VisualizationElement 10 | 11 | 12 | class TextElement(VisualizationElement): 13 | package_includes = ["TextModule.js"] 14 | js_code = "elements.push(new TextModule());" 15 | -------------------------------------------------------------------------------- /visualization/templates/cache_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ model_name }} (Mesa visualization) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 54 | 55 | 56 |
57 |
58 |
59 | 62 | 63 |
64 |
65 |
66 | 67 | 69 |
70 |
71 |

Loading...

72 |
73 | 74 |
75 | 76 |
77 |
78 | 79 | 84 |
85 |
86 | 87 | 88 | 89 | 90 | 109 | 110 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | {% for file_name in package_includes %} 152 | 153 | {% end %} 154 | {% for file_name in local_includes %} 155 | 156 | {% end %} 157 | 158 | 159 | 162 | 163 | 164 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /visualization/templates/css/bootstrap-slider.min.css: -------------------------------------------------------------------------------- 1 | /*! ======================================================= 2 | VERSION 9.8.0 3 | ========================================================= */ 4 | /*! ========================================================= 5 | * bootstrap-slider.js 6 | * 7 | * Maintainers: 8 | * Kyle Kemp 9 | * - Twitter: @seiyria 10 | * - Github: seiyria 11 | * Rohit Kalkur 12 | * - Twitter: @Rovolutionary 13 | * - Github: rovolution 14 | * 15 | * ========================================================= 16 | * 17 | * bootstrap-slider is released under the MIT License 18 | * Copyright (c) 2017 Kyle Kemp, Rohit Kalkur, and contributors 19 | * 20 | * Permission is hereby granted, free of charge, to any person 21 | * obtaining a copy of this software and associated documentation 22 | * files (the "Software"), to deal in the Software without 23 | * restriction, including without limitation the rights to use, 24 | * copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | * copies of the Software, and to permit persons to whom the 26 | * Software is furnished to do so, subject to the following 27 | * conditions: 28 | * 29 | * The above copyright notice and this permission notice shall be 30 | * included in all copies or substantial portions of the Software. 31 | * 32 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 33 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 34 | * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 35 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 36 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 37 | * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 38 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 39 | * OTHER DEALINGS IN THE SOFTWARE. 40 | * 41 | * ========================================================= */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection,.slider.slider-horizontal .slider-track-low,.slider.slider-horizontal .slider-track-high{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-tick,.slider.slider-horizontal .slider-handle{margin-left:-10px}.slider.slider-horizontal .slider-tick.triangle,.slider.slider-horizontal .slider-handle.triangle{position:relative;top:50%;transform:translateY(-50%);border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#0480be;margin-top:0}.slider.slider-horizontal .slider-tick-container{white-space:nowrap;position:absolute;top:0;left:0;width:100%}.slider.slider-horizontal .slider-tick-label-container{white-space:nowrap;margin-top:20px}.slider.slider-horizontal .slider-tick-label-container .slider-tick-label{padding-top:4px;display:inline-block;text-align:center}.slider.slider-horizontal.slider-rtl .slider-track{left:initial;right:0}.slider.slider-horizontal.slider-rtl .slider-tick,.slider.slider-horizontal.slider-rtl .slider-handle{margin-left:initial;margin-right:-10px}.slider.slider-horizontal.slider-rtl .slider-tick-container{left:initial;right:0}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;left:25%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-track-low,.slider.slider-vertical .slider-track-high{width:100%;left:0;right:0}.slider.slider-vertical .slider-tick,.slider.slider-vertical .slider-handle{margin-top:-10px}.slider.slider-vertical .slider-tick.triangle,.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#0480be;border-right-color:#0480be;margin-left:0;margin-right:0}.slider.slider-vertical .slider-tick-label-container{white-space:nowrap}.slider.slider-vertical .slider-tick-label-container .slider-tick-label{padding-left:4px}.slider.slider-vertical.slider-rtl .slider-track{left:initial;right:25%}.slider.slider-vertical.slider-rtl .slider-selection{left:initial;right:0}.slider.slider-vertical.slider-rtl .slider-tick.triangle,.slider.slider-vertical.slider-rtl .slider-handle.triangle{border-width:10px 10px 10px 0}.slider.slider-vertical.slider-rtl .slider-tick-label-container .slider-tick-label{padding-left:initial;padding-right:4px}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:-o-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:-o-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip.top{margin-top:-36px}.slider .tooltip-inner{white-space:nowrap;max-width:none}.slider .hide{display:none}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-selection.tick-slider-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0)}.slider-track-low,.slider-track-high{position:absolute;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;top:0;width:20px;height:20px;background-color:#337ab7;background-image:-webkit-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:-o-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:linear-gradient(to bottom,#149bdf 0,#0480be 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none}.slider-handle.custom{background:transparent none}.slider-handle.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick{position:absolute;width:20px;height:20px;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:none;opacity:.8;border:0 solid transparent}.slider-tick.round{border-radius:50%}.slider-tick.triangle{background:transparent none}.slider-tick.custom{background:transparent none}.slider-tick.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick.in-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0);opacity:1} -------------------------------------------------------------------------------- /visualization/templates/css/bootstrap-switch.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. 3 | * 4 | * @version v3.3.4 5 | * @homepage https://bttstrp.github.io/bootstrap-switch 6 | * @author Mattia Larentis (http://larentis.eu) 7 | * @license Apache-2.0 8 | */ 9 | 10 | .bootstrap-switch { 11 | display: inline-block; 12 | direction: ltr; 13 | cursor: pointer; 14 | border-radius: 4px; 15 | border: 1px solid; 16 | border-color: #ccc; 17 | position: relative; 18 | text-align: left; 19 | overflow: hidden; 20 | line-height: 8px; 21 | z-index: 0; 22 | -webkit-user-select: none; 23 | -moz-user-select: none; 24 | -ms-user-select: none; 25 | user-select: none; 26 | vertical-align: middle; 27 | -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 28 | -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 29 | transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; 30 | } 31 | .bootstrap-switch .bootstrap-switch-container { 32 | display: inline-block; 33 | top: 0; 34 | border-radius: 4px; 35 | -webkit-transform: translate3d(0, 0, 0); 36 | transform: translate3d(0, 0, 0); 37 | } 38 | .bootstrap-switch .bootstrap-switch-handle-on, 39 | .bootstrap-switch .bootstrap-switch-handle-off, 40 | .bootstrap-switch .bootstrap-switch-label { 41 | -webkit-box-sizing: border-box; 42 | -moz-box-sizing: border-box; 43 | box-sizing: border-box; 44 | cursor: pointer; 45 | display: table-cell; 46 | vertical-align: middle; 47 | padding: 6px 12px; 48 | font-size: 14px; 49 | line-height: 20px; 50 | } 51 | .bootstrap-switch .bootstrap-switch-handle-on, 52 | .bootstrap-switch .bootstrap-switch-handle-off { 53 | text-align: center; 54 | z-index: 1; 55 | } 56 | .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary, 57 | .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary { 58 | color: #fff; 59 | background: #337ab7; 60 | } 61 | .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info, 62 | .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info { 63 | color: #fff; 64 | background: #5bc0de; 65 | } 66 | .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success, 67 | .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success { 68 | color: #fff; 69 | background: #5cb85c; 70 | } 71 | .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning, 72 | .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning { 73 | background: #f0ad4e; 74 | color: #fff; 75 | } 76 | .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger, 77 | .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger { 78 | color: #fff; 79 | background: #d9534f; 80 | } 81 | .bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default, 82 | .bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default { 83 | color: #000; 84 | background: #eeeeee; 85 | } 86 | .bootstrap-switch .bootstrap-switch-label { 87 | text-align: center; 88 | margin-top: -1px; 89 | margin-bottom: -1px; 90 | z-index: 100; 91 | color: #333; 92 | background: #fff; 93 | } 94 | .bootstrap-switch span::before { 95 | content: "\200b"; 96 | } 97 | .bootstrap-switch .bootstrap-switch-handle-on { 98 | border-bottom-left-radius: 3px; 99 | border-top-left-radius: 3px; 100 | } 101 | .bootstrap-switch .bootstrap-switch-handle-off { 102 | border-bottom-right-radius: 3px; 103 | border-top-right-radius: 3px; 104 | } 105 | .bootstrap-switch input[type='radio'], 106 | .bootstrap-switch input[type='checkbox'] { 107 | position: absolute !important; 108 | top: 0; 109 | left: 0; 110 | margin: 0; 111 | z-index: -1; 112 | opacity: 0; 113 | filter: alpha(opacity=0); 114 | visibility: hidden; 115 | } 116 | .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on, 117 | .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off, 118 | .bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label { 119 | padding: 1px 5px; 120 | font-size: 12px; 121 | line-height: 1.5; 122 | } 123 | .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on, 124 | .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off, 125 | .bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label { 126 | padding: 5px 10px; 127 | font-size: 12px; 128 | line-height: 1.5; 129 | } 130 | .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on, 131 | .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off, 132 | .bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label { 133 | padding: 6px 16px; 134 | font-size: 18px; 135 | line-height: 1.3333333; 136 | } 137 | .bootstrap-switch.bootstrap-switch-disabled, 138 | .bootstrap-switch.bootstrap-switch-readonly, 139 | .bootstrap-switch.bootstrap-switch-indeterminate { 140 | cursor: default !important; 141 | } 142 | .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on, 143 | .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on, 144 | .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on, 145 | .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off, 146 | .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off, 147 | .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off, 148 | .bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label, 149 | .bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label, 150 | .bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label { 151 | opacity: 0.5; 152 | filter: alpha(opacity=50); 153 | cursor: default !important; 154 | } 155 | .bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container { 156 | -webkit-transition: margin-left 0.5s; 157 | -o-transition: margin-left 0.5s; 158 | transition: margin-left 0.5s; 159 | } 160 | .bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on { 161 | border-bottom-left-radius: 0; 162 | border-top-left-radius: 0; 163 | border-bottom-right-radius: 3px; 164 | border-top-right-radius: 3px; 165 | } 166 | .bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off { 167 | border-bottom-right-radius: 0; 168 | border-top-right-radius: 0; 169 | border-bottom-left-radius: 3px; 170 | border-top-left-radius: 3px; 171 | } 172 | .bootstrap-switch.bootstrap-switch-focused { 173 | border-color: #66afe9; 174 | outline: 0; 175 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); 176 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); 177 | } 178 | .bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label, 179 | .bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label { 180 | border-bottom-right-radius: 3px; 181 | border-top-right-radius: 3px; 182 | } 183 | .bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label, 184 | .bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label { 185 | border-bottom-left-radius: 3px; 186 | border-top-left-radius: 3px; 187 | } 188 | -------------------------------------------------------------------------------- /visualization/templates/css/bootstrap-switch.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. 3 | * 4 | * @version v3.3.4 5 | * @homepage https://bttstrp.github.io/bootstrap-switch 6 | * @author Mattia Larentis (http://larentis.eu) 7 | * @license Apache-2.0 8 | */ 9 | 10 | .bootstrap-switch{display:inline-block;direction:ltr;cursor:pointer;border-radius:4px;border:1px solid #ccc;position:relative;text-align:left;overflow:hidden;line-height:8px;z-index:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.bootstrap-switch .bootstrap-switch-container{display:inline-block;top:0;border-radius:4px;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;cursor:pointer;display:table-cell;vertical-align:middle;padding:6px 12px;font-size:14px;line-height:20px}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{color:#fff;background:#5bc0de}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{color:#fff;background:#5cb85c}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f0ad4e;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{color:#fff;background:#d9534f}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{color:#000;background:#eee}.bootstrap-switch .bootstrap-switch-label{text-align:center;margin-top:-1px;margin-bottom:-1px;z-index:100;color:#333;background:#fff}.bootstrap-switch span::before{content:"\200b"}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:3px;border-top-left-radius:3px}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch input[type=radio],.bootstrap-switch input[type=checkbox]{position:absolute!important;top:0;left:0;margin:0;z-index:-1;opacity:0;filter:alpha(opacity=0);visibility:hidden}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{padding:1px 5px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{padding:5px 10px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{padding:6px 16px;font-size:18px;line-height:1.3333333}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default!important}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{opacity:.5;filter:alpha(opacity=50);cursor:default!important}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 3px 3px 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:3px 0 0 3px}.bootstrap-switch.bootstrap-switch-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:3px;border-top-left-radius:3px} -------------------------------------------------------------------------------- /visualization/templates/css/visualization.css: -------------------------------------------------------------------------------- 1 | .model-parameter { 2 | margin-bottom: 15px; 3 | } 4 | 5 | .navbar-brand 6 | { 7 | position: absolute; 8 | width: 100%; 9 | left: 0; 10 | text-align: center; 11 | margin:0 auto; 12 | } 13 | 14 | .navbar-toggle { 15 | z-index:3; 16 | } 17 | 18 | html { 19 | height: auto !important; 20 | padding-bottom: 200px !important; 21 | } 22 | 23 | .btn-pad { 24 | padding-top: 1px; 25 | padding-bottom: 1px; 26 | width:100%; 27 | border-color: #333 !important; 28 | background-image: linear-gradient(to bottom,#999 0,#BBB 100%) !important; 29 | color: #000; 30 | } 31 | 32 | .btn-pad:hover { 33 | background-position: 0 0; 34 | background-color: #222 !important; 35 | background-image: linear-gradient(to bottom,#666 0,#999 100%) !important; 36 | } 37 | #chartjs-tooltip { 38 | opacity: 1; 39 | position: absolute; 40 | background: rgba(0, 0, 0, .7); 41 | color: white; 42 | border-radius: 3px; 43 | -webkit-transition: all .1s ease; 44 | transition: all .1s ease; 45 | pointer-events: none; 46 | -webkit-transform: translate(-50%, 0); 47 | transform: translate(-50%, 0); 48 | } 49 | 50 | .chartjs-tooltip-key { 51 | display: inline-block; 52 | width: 10px; 53 | height: 10px; 54 | margin-right: 10px; 55 | } 56 | 57 | 58 | 59 | html:after { 60 | 61 | /* common custom values */ 62 | content: "ALPHA"; 63 | font-size: 720%; /* font size */ 64 | color: rgba(0, 0, 0, .1); 65 | /* alpha, could be even rgba(0,0,0,.05) */ 66 | 67 | /* rest of the logic */ 68 | z-index: 9999; 69 | cursor: default; 70 | display: block; 71 | position: fixed; 72 | top: 33%; 73 | right: 0; 74 | bottom: 0; 75 | left: 15%; 76 | font-family: sans-serif; 77 | font-weight: bold; 78 | font-style: italic; 79 | text-align: center; 80 | line-height: 100%; 81 | 82 | pointer-events: none; 83 | 84 | -webkit-transform: rotate(-30deg); 85 | -moz-transform: rotate(-30deg); 86 | -ms-transform: rotate(-30deg); 87 | -o-transform: rotate(-30deg); 88 | transform: rotate(-30deg); 89 | 90 | -webkit-user-select: none; 91 | -moz-user-select: none; 92 | -ms-user-select: none; 93 | user-select: none; 94 | 95 | 96 | } 97 | 98 | html, 99 | body { 100 | overflow-x: hidden; /* Prevent scroll on narrow devices */ 101 | } 102 | body { 103 | padding-top: 70px; 104 | } 105 | footer { 106 | padding: 30px 0; 107 | } 108 | 109 | /* 110 | * Off Canvas 111 | * -------------------------------------------------- 112 | */ 113 | @media screen and (max-width: 767px) { 114 | .row-offcanvas { 115 | position: relative; 116 | -webkit-transition: all .25s ease-out; 117 | -o-transition: all .25s ease-out; 118 | transition: all .25s ease-out; 119 | } 120 | 121 | .sidebar-offcanvas { 122 | position: absolute; 123 | width: 60%; /* 6 columns */ 124 | display: none; 125 | } 126 | 127 | .row-offcanvas-right { 128 | right: 0; 129 | } 130 | 131 | .row-offcanvas-right.active 132 | .sidebar-offcanvas { 133 | display: block; 134 | right: 0; 135 | top: 0; 136 | z-index: 1; 137 | } 138 | 139 | .row-offcanvas-right.active { 140 | 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /visualization/templates/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/visualization/templates/favicon.ico -------------------------------------------------------------------------------- /visualization/templates/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/visualization/templates/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /visualization/templates/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/visualization/templates/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /visualization/templates/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/visualization/templates/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /visualization/templates/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Synthetixio/simulation/0e278548bc87e0f4a1bce4c4beab1214ad999a76/visualization/templates/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /visualization/templates/havven_description.html: -------------------------------------------------------------------------------- 1 |

An agent-based model of the Havven stablecoin system. This model includes the basic market functionality of Havven, an exchange, and a place for the market agents to live and interact.

2 |

The aim is to stabilise the nomin price, but we would also like to measure other quantities including liquidity, volatility, wealth concentration, velocity of money and so on.

3 |

Analysis:

4 |

Currently the model’s only sources of havven liquidity are Randomizers and MarketMakers, while Bankers are the only ones who want to purchase it (other than short term trades by arbitrageurs). As such, if there are too many Randomizers, without enough Bankers, the price of havvens will tank, taking the havven/nomin price with it.

5 |

If the bankers have more backing than the randomizers, the price will shoot up, until the randomizers run out of havvens, or the bankers run out of fiat, which then causes the price to dip as the supply of havvens dries up. At this point the only reason the price goes up is due to arbitrage opportunities arising from nomin shorters, when the nomin price dips below their nomin buy threshold, or rises above their nomin sell threshold.

6 |

After this the only real action happening is with the Merchant/buyers, who trade solely in fiat/nomins. Who after a significant while, break the wall created by the nomin shorters, causing the nomin price to tank. The only reason they build enough bank to break the wall is due to the buyers being the only actors in the whole system who have an accumulating wealth.

7 |

All market behaviours are accelerated/slowed by increasing/decreasing the number of actors.

8 |
9 |
The model is still in the development stage. The source will be released at a future date, for full customisation options.
10 | -------------------------------------------------------------------------------- /visualization/templates/js/BarGraphModule.js: -------------------------------------------------------------------------------- 1 | // BarGraphModule.js 2 | 3 | var BarGraphModule = function (group, title, desc, label, width, height) { 4 | let group_id = (group).replace(/[^a-zA-Z]/g, ""); 5 | let graph_id = (title).replace(/[^a-zA-Z]/g, ""); 6 | // Create the elements 7 | // var button = $(''); 8 | // button.tooltip(); 9 | 10 | var div = $(""); 11 | 12 | $("#elements").append(div); 13 | // Create the context and the drawing controller: 14 | 15 | 16 | if ($("#"+group_id)[0] === undefined) { 17 | var group_link = $("" + group + ""); 18 | $("#sidebar-list").append(group_link); 19 | } 20 | 21 | // Create the chart object 22 | var chart = Highcharts.chart(graph_id, { 23 | title: { 24 | text: title 25 | }, 26 | chart: { 27 | animation: false, 28 | height: 250, 29 | min : 0 30 | }, 31 | credits: { 32 | enabled: false 33 | }, 34 | 35 | }); 36 | var chart_setup = false; 37 | 38 | this.render = function (force_draw, data) { 39 | 40 | if (div.hasClass("hidden")) { 41 | chart.was_hidden = true; 42 | return false; 43 | } 44 | 45 | let new_data; 46 | 47 | if (data.length < 1) { 48 | return false; 49 | } 50 | 51 | if (chart_setup === false && data.length > 0) { 52 | chart = this.create_chart(data[0]); 53 | chart_setup = true; 54 | } 55 | 56 | if (chart.was_hidden || force_draw) { 57 | if (data.length > 1) { 58 | new_data = data[data.length - 1]; 59 | } else { 60 | new_data = []; 61 | for (let i = 4; i < data[0].length; i++) { 62 | new_data.push(data[0][i]) 63 | } 64 | } 65 | 66 | if (new_data.length >= 1) { 67 | for (let i = 0; i < new_data.length; i++) { 68 | let _data = []; 69 | for (let j = 0; j < new_data[i].length; j++) { 70 | _data.push(this.round(new_data[i][j])) 71 | } 72 | chart.series[i].setData(_data); 73 | } 74 | } 75 | } 76 | chart.was_hidden = false; 77 | }; 78 | 79 | this.reset = function () { 80 | chart_setup = false; 81 | }; 82 | 83 | this.round = function (value) { 84 | return Math.floor(value*1000)/1000 85 | }; 86 | 87 | this.create_chart = function(label_data) { 88 | let options = chart.options; 89 | 90 | let data_labels = label_data[0]; 91 | let data_colors = label_data[1]; 92 | let data_stacks = label_data[2]; 93 | 94 | // player names 95 | options.xAxis.categories = label_data[3]; 96 | options.series = []; 97 | 98 | for (let i = 0; i < label_data[0].length; i++) { 99 | options.series.push({ 100 | name: data_labels[i], 101 | color: data_colors[i], 102 | stack: data_stacks[i], 103 | data: [] 104 | }) 105 | } 106 | 107 | options.plotOptions = { 108 | column: { 109 | stacking: 'normal', 110 | pointPadding: 0.2, 111 | borderWidth: 0 112 | } 113 | }; 114 | 115 | options.chart = { 116 | type: 'column', 117 | animation: false, 118 | height: 300 119 | }; 120 | 121 | options.tooltip = { 122 | shared:true, 123 | followPointer: true, 124 | formatter: 125 | function () { 126 | let result = '' + chart.options.xAxis.categories[this.x] + '
'; 127 | for (let i in chart.series) { 128 | // could add some variable for using absolute values in the constructor, but for now 129 | // all the graphs want to. 130 | result += chart.series[i].name + ': ' + Math.abs(chart.series[i].data[this.x].y) + '
'; 131 | } 132 | return result; 133 | } 134 | }; 135 | 136 | return Highcharts.chart(chart.renderTo.id, options); 137 | } 138 | }; 139 | -------------------------------------------------------------------------------- /visualization/templates/js/CandleStickModule.js: -------------------------------------------------------------------------------- 1 | var CandleStickModule = function(group, title, desc, label, width, height, line_colour, bar_colour) { 2 | let group_id = (group).replace(/[^a-zA-Z]/g, ""); 3 | let graph_id = (title).replace(/[^a-zA-Z]/g, ""); 4 | // Create the elements 5 | // var button = $(''); 6 | // button.tooltip(); 7 | 8 | var div = $(""); 9 | 10 | $("#elements").append(div); 11 | // Create the context and the drawing controller: 12 | 13 | 14 | if ($("#"+group_id)[0] === undefined) { 15 | var group_link = $("" + group + ""); 16 | $("#sidebar-list").append(group_link); 17 | } 18 | 19 | // Prep the chart properties and series: 20 | 21 | var chart = Highcharts.stockChart(graph_id, { 22 | rangeSelector: { 23 | selected: 30, 24 | inputEnabled: false, 25 | buttons: [{ 26 | type: 'millisecond', 27 | count: 30, 28 | text: '30' 29 | }, 30 | { 31 | type: 'millisecond', 32 | count: 100, 33 | text: '100' 34 | },{ 35 | type: 'millisecond', 36 | count: 200, 37 | text: '200' 38 | },{ 39 | type: 'millisecond', 40 | count: 400, 41 | text: '400' 42 | },{ 43 | type: 'all', 44 | text: 'All' 45 | } 46 | ] 47 | }, 48 | 49 | title: { 50 | text: title 51 | }, 52 | 53 | xAxis: { 54 | visible: false, 55 | dateTimeLabelFormats: { 56 | millisecond: "%L", 57 | second: "%S%L", 58 | minute: "%M%S%L" 59 | } 60 | }, 61 | 62 | yAxis: [ 63 | { 64 | title: { 65 | text:"Candle Data" 66 | }, 67 | }, 68 | { 69 | title: { 70 | text: "Volume Data" 71 | }, 72 | opposite: true 73 | } 74 | ], 75 | 76 | tooltip: { 77 | animation: false, 78 | shared: true, 79 | dateTimeLabelFormats: { 80 | millisecond: "%L", 81 | second:"%S%L", 82 | minute:"%M%S%L", 83 | hour:"%L", 84 | day:"%L", 85 | week:"%L", 86 | month:"%L", 87 | year:"%L" 88 | }, 89 | }, 90 | 91 | navigator: { 92 | xAxis: { 93 | visible: false 94 | }, 95 | series: { 96 | color: line_colour, 97 | } 98 | }, 99 | chart: { 100 | animation: false, 101 | height: 300 102 | }, 103 | credits: { 104 | enabled: false 105 | }, 106 | 107 | dataGrouping: { 108 | dateTimeLabelFormats: { 109 | millisecond: ['%S%L', '%S%L', '-%S%L'], 110 | second: ['%S%L', '%S%L', '-%S%L'], 111 | minute: ['%M%S%L', '%M%S%L', '-%M%S%L'], 112 | hour: ['%M%S%L', '%M%S%L', '-%M%S%L'], 113 | day: ['%M%S%L', '%M%S%L', '-%M%S%L'], 114 | week: ['%M%S%L', '%M%S%L', '-%M%S%L'], 115 | month: ['%M%S%L', '%M%S%L', '-%M%S%L'], 116 | year: ['%M%S%L', '%M%S%L', '-%M%S%L'] 117 | }, 118 | }, 119 | 120 | series: [{ 121 | type: 'candlestick', 122 | name: 'Havven Candle Data', 123 | min : 0, 124 | data: [], 125 | upColor: '#0F0', 126 | color: '#F00', 127 | dataGrouping: { 128 | dateTimeLabelFormats: { 129 | millisecond: ['%S%L', '%S%L', '-%S%L'], 130 | second: ['%S%L', '%S%L', '-%S%L'], 131 | minute: ['%M%S%L', '%M%S%L', '-%M%S%L'], 132 | hour: ['%M%S%L', '%M%S%L', '-%M%S%L'], 133 | day: ['%M%S%L', '%M%S%L', '-%M%S%L'], 134 | week: ['%M%S%L', '%M%S%L', '-%M%S%L'], 135 | month: ['%M%S%L', '%M%S%L', '-%M%S%L'], 136 | year: ['%M%S%L', '%M%S%L', '-%M%S%L'] 137 | }, 138 | 139 | }, 140 | 141 | }, { 142 | type: 'line', 143 | name: 'Rolling Average', 144 | data: [], 145 | color: line_colour, 146 | 147 | }, { 148 | type: 'column', 149 | name: 'Volume', 150 | data: [], 151 | zIndex: -1, 152 | yAxis: 1, 153 | color: bar_colour 154 | } 155 | ] 156 | }); 157 | 158 | this.render = function(force_draw, data) { 159 | 160 | if (div.hasClass("hidden")) { 161 | chart.was_hidden = true; 162 | return false; 163 | } 164 | 165 | if (data.length < 1) { 166 | return false; 167 | } 168 | 169 | if (force_draw || chart.was_hidden) { 170 | 171 | let candle_data = []; 172 | let line_data = []; 173 | let bar_data = []; 174 | for (let i = 0; i < data.length; i++) { 175 | candle_data.push([i, data[i][0][0], data[i][0][2], data[i][0][3], data[i][0][1]]); 176 | line_data.push([i, data[i][1]]); 177 | bar_data.push([i, data[i][2]]); 178 | } 179 | 180 | 181 | let candle_series = chart.series[0]; 182 | candle_series.setData(candle_data); 183 | let line_series = chart.series[1]; 184 | line_series.setData(line_data); 185 | let bar_series = chart.series[2]; 186 | bar_series.setData(bar_data); 187 | 188 | } 189 | 190 | if (data.length === 35 || chart.was_hidden) { 191 | chart.rangeSelector.clickButton(4); 192 | chart.rangeSelector.clickButton(0); 193 | } else if (data.length < 35) { 194 | chart.rangeSelector.clickButton(0); 195 | chart.rangeSelector.clickButton(4); 196 | } 197 | 198 | chart.was_hidden = false; 199 | }; 200 | 201 | this.reset = function() { 202 | for (let i=0; i'+title+''); 6 | // button.tooltip(); 7 | 8 | var div = $(""); 9 | 10 | $("#elements").append(div); 11 | // Create the context and the drawing controller: 12 | 13 | 14 | if ($("#"+group_id)[0] === undefined) { 15 | var group_link = $("" + group + ""); 16 | $("#sidebar-list").append(group_link); 17 | } 18 | 19 | var datasets = []; 20 | for (var i in series) { 21 | var s = series[i]; 22 | var new_series = { 23 | name: s.Label, 24 | color: s.Color, 25 | fill: false, 26 | }; 27 | datasets.push(new_series); 28 | } 29 | 30 | // Create the context and the drawing controller: 31 | var chart = Highcharts.chart(graph_id, { 32 | plotOptions: { 33 | line: { 34 | marker: { 35 | enabled: false 36 | } 37 | } 38 | }, 39 | 40 | title: { 41 | text: title 42 | }, 43 | 44 | legend: { 45 | layout: 'vertical', 46 | align: 'right', 47 | verticalAlign: 'middle' 48 | }, 49 | 50 | tooltip: { 51 | shared: true 52 | }, 53 | 54 | series: datasets, 55 | chart: { 56 | animation: false, 57 | height: 250, 58 | min : 0 59 | }, 60 | credits: { 61 | enabled: false 62 | }, 63 | 64 | }); 65 | 66 | this.render = function(force_draw, data) { 67 | 68 | if (div.hasClass("hidden")) { 69 | chart.was_hidden = true; 70 | return false; 71 | } 72 | 73 | if (data.length < 1) { 74 | return false; 75 | } 76 | 77 | if (force_draw || chart.was_hidden || data.length < 5) { 78 | for (let j in data[0]) { 79 | let _data = []; 80 | for (let i = 0; i < data.length; i++) { 81 | _data.push([i, data[i][j]]); 82 | } 83 | chart.series[j].setData(_data) 84 | } 85 | 86 | } 87 | 88 | chart.was_hidden = false; 89 | }; 90 | 91 | this.reset = function() { 92 | }; 93 | }; -------------------------------------------------------------------------------- /visualization/templates/js/DepthGraphModule.js: -------------------------------------------------------------------------------- 1 | // DepthGraphModule.js 2 | 3 | var DepthGraphModule = function (group, title, desc, label, width, height) { 4 | let group_id = (group).replace(/[^a-zA-Z]/g, ""); 5 | let graph_id = (title).replace(/[^a-zA-Z]/g, ""); 6 | // Create the elements 7 | // var button = $(''); 8 | // button.tooltip(); 9 | 10 | var div = $(""); 11 | 12 | $("#elements").append(div); 13 | // Create the context and the drawing controller: 14 | 15 | 16 | if ($("#"+group_id)[0] === undefined) { 17 | var group_link = $("" + group + ""); 18 | $("#sidebar-list").append(group_link); 19 | } 20 | 21 | var chart = Highcharts.chart(graph_id, { 22 | chart: { 23 | animation: false, 24 | height: 250, 25 | type: 'area' 26 | }, 27 | title: { 28 | text: title 29 | }, 30 | subtitle: { 31 | text: '' 32 | }, 33 | credits: { 34 | enabled: false 35 | }, 36 | 37 | xAxis: { 38 | allowDecimals: true, 39 | labels: { 40 | formatter: function () { 41 | return this.value; // clean, unformatted number for year 42 | } 43 | } 44 | }, 45 | yAxis: { 46 | title: { 47 | text: 'Volume' 48 | }, 49 | labels: { 50 | formatter: function () { 51 | return this.value; 52 | } 53 | }, 54 | min : 0 55 | }, 56 | plotOptions: { 57 | area: { 58 | marker: { 59 | enabled: false, 60 | symbol: 'circle', 61 | radius: 2, 62 | states: { 63 | hover: { 64 | enabled: true 65 | } 66 | } 67 | } 68 | } 69 | }, 70 | series: [ 71 | { 72 | name: 'Asks', 73 | data: [], 74 | color: 'rgba(120,255,120,0.2)', 75 | lineColor:'green', 76 | }, { 77 | name: 'Bids', 78 | data: [], 79 | color:'rgba(255,0,0,0.2)', 80 | lineColor:'red' 81 | }] 82 | }); 83 | 84 | this.render = function (step, new_data) { 85 | if (new_data.length > 0) { 86 | new_data = new_data[new_data.length-1] 87 | } else { 88 | return false; 89 | } 90 | let price_range = 1.0; 91 | let curr_price = new_data[0]; 92 | let bids = new_data[1]; 93 | let asks = new_data[2]; 94 | 95 | // data is sorted by rate, in the form [(rate, quantity) ... ] 96 | let max_bid = 0, min_ask = 0; 97 | if (bids.length > 0) { 98 | max_bid = bids[0][0]; 99 | } 100 | 101 | if (asks.length > 0) { 102 | min_ask = asks[0][0]; 103 | } 104 | 105 | let avg_price = (max_bid + min_ask) / 2; 106 | 107 | let cumulative_quant = 0; 108 | let added_bid = false; 109 | let _bid_data = []; 110 | let _ask_data = []; 111 | for (let i in bids) { 112 | let price = bids[i][0]; 113 | if (price < avg_price * (1 - price_range)) break; 114 | added_bid = true; 115 | cumulative_quant += bids[i][1]; 116 | _bid_data.unshift( 117 | [this.round(price), this.round(cumulative_quant)] 118 | ); 119 | } 120 | if (added_bid) { 121 | _bid_data.unshift( 122 | [this.round(curr_price * (1 - price_range)), _bid_data[0][1]] 123 | ); 124 | } else { 125 | _bid_data.unshift( 126 | [this.round(curr_price * (1 - price_range)), 0] 127 | ); 128 | } 129 | 130 | cumulative_quant = 0; 131 | 132 | // push ask data to the chart 133 | let added_ask = false; 134 | for (let i in asks) { 135 | let price = asks[i][0]; 136 | if (price > avg_price * (1 + price_range)) break; 137 | added_ask = true; 138 | cumulative_quant += asks[i][1]; 139 | _ask_data.push( 140 | [this.round(price), this.round(cumulative_quant)] 141 | ); 142 | } 143 | 144 | if (added_ask) { 145 | _ask_data.push( 146 | [this.round(curr_price * (1 + price_range)), 147 | _ask_data[_ask_data.length-1][1]] 148 | ); 149 | } else { 150 | _ask_data.push( 151 | [this.round(curr_price * (1 + price_range)), 0] 152 | ); 153 | } 154 | 155 | chart.series[0].setData(_ask_data); 156 | chart.series[1].setData(_bid_data); 157 | }; 158 | 159 | this.reset = function () { 160 | }; 161 | 162 | this.round = function (value) { 163 | return Math.floor(value*10000)/10000 164 | } 165 | }; 166 | -------------------------------------------------------------------------------- /visualization/templates/js/TextModule.js: -------------------------------------------------------------------------------- 1 | var TextModule = function() { 2 | var tag = "

"; 3 | var text = $(tag)[0]; 4 | $("body").append(text); 5 | 6 | this.render = function(data) { 7 | $(text).html(data); 8 | }; 9 | 10 | this.reset = function() { 11 | $(text).html(""); 12 | }; 13 | }; -------------------------------------------------------------------------------- /visualization/templates/modular_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ model_name }} (Mesa visualization) 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 54 | 55 | 56 |
57 |
58 |
59 | 62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 |
71 |
72 | 73 |
74 |
75 | 76 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | 106 | 107 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | {% for file_name in package_includes %} 144 | 145 | {% end %} 146 | {% for file_name in local_includes %} 147 | 148 | {% end %} 149 | 150 | 151 | 154 | 155 | 156 | 157 | 162 | 163 | -------------------------------------------------------------------------------- /visualization/text_visualization.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Text Visualization 4 | ================== 5 | 6 | Base classes for ASCII-only visualizations of a model. 7 | These are useful for quick debugging, and can readily be rendered in an IPython 8 | Notebook or via text alone in a browser window. 9 | 10 | Classes: 11 | 12 | TextVisualization: Class meant to wrap around a Model object and render it 13 | in some way using Elements, which are stored in a list and rendered in that 14 | order. Each element, in turn, renders a particular piece of information as 15 | text. 16 | 17 | TextElement: Parent class for all other ASCII elements. render() returns its 18 | representative string, which can be printed via the overloaded __str__ method. 19 | 20 | TextData: Uses getattr to get the value of a particular property of a model 21 | and prints it, along with its name. 22 | 23 | TextGrid: Prints a grid, assuming that the value of each cell maps to exactly 24 | one ASCII character via a converter method. This (as opposed to a dictionary) 25 | is used so as to allow the method to access Agent internals, as well as to 26 | potentially render a cell based on several values (e.g. an Agent grid and a 27 | Patch value grid). 28 | 29 | """ 30 | # Pylint instructions: allow single-character variable names. 31 | # pylint: disable=invalid-name 32 | 33 | 34 | class TextVisualization: 35 | """ ASCII-Only visualization of a model. 36 | 37 | Properties: 38 | 39 | model: The underlying model object to be visualized. 40 | elements: List of visualization elements, which will be rendered 41 | in the order they are added. 42 | 43 | """ 44 | def __init__(self, model): 45 | """ Create a new Text Visualization object. """ 46 | self.model = model 47 | self.elements = [] 48 | 49 | def render(self): 50 | """ Render all the text elements, in order. """ 51 | for element in self.elements: 52 | print(element) 53 | 54 | def step(self): 55 | """ Advance the model by a step and print the results. """ 56 | self.model.step() 57 | self.render() 58 | 59 | 60 | class TextElement: 61 | """ Base class for all TextElements to render. 62 | 63 | Methods: 64 | render: 'Renders' some data into ASCII and returns. 65 | __str__: Displays render() by default. 66 | """ 67 | 68 | def __init__(self): 69 | pass 70 | 71 | def render(self): 72 | """ Render the element as text. """ 73 | return "Placeholder!" 74 | 75 | def __str__(self): 76 | return self.render() 77 | 78 | 79 | class TextData(TextElement): 80 | """ Prints the value of one particular variable from the base model. """ 81 | def __init__(self, model, var_name): 82 | """ Create a new data renderer. """ 83 | self.model = model 84 | self.var_name = var_name 85 | 86 | def render(self): 87 | return self.var_name + ": " + str(getattr(self.model, self.var_name)) 88 | 89 | 90 | class TextGrid(TextElement): 91 | """ Class for creating an ASCII visualization of a basic grid object. 92 | 93 | By default, assume that each cell is represented by one character, and 94 | that empty cells are rendered as ' ' characters. When printed, the TextGrid 95 | results in a width x height grid of ascii characters. 96 | 97 | Properties: 98 | grid: The underlying grid object. 99 | 100 | """ 101 | grid = None 102 | 103 | def __init__(self, grid, converter): 104 | """ Create a new ASCII grid visualization. 105 | 106 | Args: 107 | grid: The underlying Grid object. 108 | converter: function for converting the content of each cell 109 | to ascii 110 | """ 111 | self.grid = grid 112 | 113 | @staticmethod 114 | def converter(x): 115 | """ Text content of cells. """ 116 | return 'X' 117 | 118 | def render(self): 119 | """ What to show when printed. """ 120 | viz = "" 121 | for y in range(self.grid.height): 122 | for x in range(self.grid.width): 123 | c = self.grid[y][x] 124 | if c is None: 125 | viz += ' ' 126 | else: 127 | viz += self.converter(c) 128 | viz += '\n' 129 | return viz 130 | -------------------------------------------------------------------------------- /visualization/userparam.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class UserSettableParameter: 5 | """ A class for providing options to a visualization for a given parameter. 6 | 7 | UserSettableParameter can be used instead of keyword arguments when specifying model parameters in an 8 | instance of a `ModularServer` so that the parameter can be adjusted in the UI without restarting the server. 9 | 10 | Validation of correctly-specified params happens on startup of a `ModularServer`. Each param is handled 11 | individually in the UI and sends callback events to the server when an option is updated. That option is then 12 | re-validated, in the `value.setter` property method to ensure input is correct from the UI to `reset_model` 13 | callback. 14 | 15 | Parameter types include: 16 | - 'number' - a simple numerical input 17 | - 'checkbox' - boolean checkbox 18 | - 'choice' - String-based dropdown input, for selecting choices within a model 19 | - 'slider' - A number-based slider input with settable increment 20 | - 'static_text' - A non-input textbox for displaying model info. 21 | 22 | Examples: 23 | 24 | # Simple number input 25 | number_option = UserSettableParameter('number', 'My Number', value=123) 26 | 27 | # Checkbox input 28 | boolean_option = UserSettableParameter('checkbox', 'My Boolean', value=True) 29 | 30 | # Choice input 31 | choice_option = UserSettableParameter('choice', 'My Choice', value='Default choice', 32 | choices=['Default Choice', 'Alternate Choice']) 33 | 34 | # Slider input 35 | slider_option = UserSettableParameter('slider', 'My Slider', value=123, min_value=10, max_value=200, step=0.1) 36 | 37 | # Static text 38 | static_text = UserSettableParameter('static_text', value="This is a descriptive textbox") 39 | """ 40 | 41 | NUMBER = 'number' 42 | CHECKBOX = 'checkbox' 43 | CHOICE = 'choice' 44 | SLIDER = 'slider' 45 | STATIC_TEXT = 'static_text' 46 | AGENT_FRACTIONS = 'agent_fractions' 47 | 48 | TYPES = (NUMBER, CHECKBOX, CHOICE, SLIDER, STATIC_TEXT, AGENT_FRACTIONS) 49 | 50 | _ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'" 51 | 52 | def __init__( 53 | self, param_type=None, name='', value=None, min_value=None, max_value=None, 54 | step=1, choices=list(), description=None 55 | ): 56 | if param_type not in self.TYPES: 57 | raise ValueError("{} is not a valid Option type".format(param_type)) 58 | self.param_type = param_type 59 | self.name = name 60 | self._value = value 61 | self.min_value = min_value 62 | self.max_value = max_value 63 | self.step = step 64 | self.choices = choices 65 | self.description = description 66 | self.got_set = False 67 | 68 | # Validate option types to make sure values are supplied properly 69 | msg = self._ERROR_MESSAGE.format(self.param_type, name) 70 | valid = True 71 | 72 | if self.param_type == self.NUMBER: 73 | valid = not (self.value is None) 74 | 75 | elif self.param_type == self.SLIDER: 76 | valid = not (self.value is None or self.min_value is None or self.max_value is None) 77 | 78 | elif self.param_type == self.CHOICE: 79 | valid = not (self.value is None or len(self.choices) == 0) 80 | 81 | elif self.param_type == self.CHECKBOX: 82 | valid = isinstance(self.value, bool) 83 | 84 | elif self.param_type == self.STATIC_TEXT: 85 | valid = isinstance(self.value, str) 86 | 87 | elif self.param_type == self.AGENT_FRACTIONS: 88 | if value is not None: 89 | valid = isinstance(value, dict) 90 | self.got_set = True 91 | else: 92 | # randomize the data if given None 93 | self._value = {} 94 | self.randomize_agents() 95 | valid = True 96 | 97 | if not valid: 98 | raise ValueError(msg) 99 | 100 | @property 101 | def value(self): 102 | if not self.got_set and self.param_type == self.AGENT_FRACTIONS: 103 | self.randomize_agents() 104 | return self._value 105 | 106 | @value.setter 107 | def value(self, value): 108 | self._value = value 109 | self.got_set = True 110 | if self.param_type == self.SLIDER: 111 | if self._value < self.min_value: 112 | self._value = self.min_value 113 | elif self._value > self.max_value: 114 | self._value = self.max_value 115 | elif self.param_type == self.CHOICE: 116 | if self._value not in self.choices: 117 | print("Selected choice value not in available choices, selected first choice from 'choices' list") 118 | self._value = self.choices[0] 119 | elif self.param_type == self.AGENT_FRACTIONS: 120 | for item in value: 121 | self._value[item[0]] = item[1] 122 | 123 | @property 124 | def json(self): 125 | result = self.__dict__.copy() 126 | result['value'] = result.pop('_value') # Return _value as value, value is the same 127 | return result 128 | 129 | def randomize_agents(self): 130 | """Randomize the agent initial values""" 131 | # import here to avoid circular reference 132 | from agents import player_names 133 | v = { 134 | i: random.random() / len(player_names) for i in player_names 135 | } 136 | if 'Merchant' in v: 137 | v['Merchant'] = 0 138 | if 'NaiveSpeculator' in v: 139 | v['NaiveSpeculator'] = 0 140 | # total should be < 1, dividing by total should make total 1. 141 | total = sum(v[i] for i in v) 142 | for item in v: 143 | self._value[item] = v[item] / total 144 | -------------------------------------------------------------------------------- /visualization/visualization_element.py: -------------------------------------------------------------------------------- 1 | 2 | class VisualizationElement: 3 | """ 4 | Defines an element of the visualization. 5 | 6 | Attributes: 7 | package_includes: A list of external JavaScript files to include that 8 | are part of the Mesa packages. 9 | local_includes: A list of JavaScript files that are local to the 10 | directory that the server is being run in. 11 | js_code: A JavaScript code string to instantiate the element. 12 | 13 | Methods: 14 | render: Takes a model object, and produces JSON data which can be sent 15 | to the client. 16 | 17 | """ 18 | 19 | package_includes = [] 20 | local_includes = [] 21 | js_code = '' 22 | render_args = {} 23 | 24 | def __init__(self): 25 | pass 26 | 27 | def render(self, model): 28 | """ Build visualization data from a model object. 29 | 30 | Args: 31 | model: A model object 32 | 33 | Returns: 34 | A JSON-ready object. 35 | 36 | """ 37 | return "VisualizationElement goes here." 38 | --------------------------------------------------------------------------------