├── .formatter.exs ├── .gitignore ├── README.md ├── bin └── run ├── config └── config.exs ├── img.png ├── lib └── apxr │ ├── application.ex │ ├── exchange.ex │ ├── liquidity_consumer.ex │ ├── market.ex │ ├── market_maker.ex │ ├── mean_reversion_trader.ex │ ├── momentum_trader.ex │ ├── my_trader.ex │ ├── nimble_csv.ex │ ├── noise_trader.ex │ ├── order.ex │ ├── orderbook_event.ex │ ├── progress_bar.ex │ ├── reporting_service.ex │ ├── run_supervisor.ex │ ├── simulation.ex │ ├── trader.ex │ └── trader_supervisor.ex ├── mix.exs ├── mix.lock ├── test ├── benchmark.exs ├── exchange_test.exs ├── nimble_csv_test.exs ├── profile.exs └── test_helper.exs └── validate.ipynb /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Ignore .fetch files in case you like to edit your project deps locally. 11 | /.fetch 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | apxr-*.tar 21 | 22 | # Ignore CSV files 23 | *.csv 24 | 25 | # Ignore asdf versions file 26 | *.tool-versions 27 | 28 | # Jupyter 29 | *.ipynb_checkpoints -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APXR 2 | 3 | A platform for the testing and optimisation of trading algorithms. 4 | 5 | -------------------- 6 | ### Related publications 7 | 8 | High frequency trading strategies, market fragility and price spikes: an agent based model perspective. 9 | McGroarty, F., Booth, A., Gerding, E. et al. Ann Oper Res (2018). https://doi.org/10.1007/s10479-018-3019-4 10 | 11 | Note: This paper fails to provide several parameters, namely: the composition of the Agents, the wealth parameter W in the Momentum trader logic, the volume parameter V minus in the MarketMaker trader logic and the number of standard deviations K in the MeanReversion trader logic. 12 | 13 | -------------------- 14 | ### Table of Contents 15 | 16 | * [Requirements](#requirements) 17 | * [Installation](#installation) 18 | * [Quick start](#quick-start) 19 | * [Introduction](#introduction) 20 | * [Architecture](#architecture) 21 | * [Configuration](#configuration) 22 | * [Outputs](#outputs) 23 | * [Validation](#validation) 24 | * [Future work](#future-work) 25 | * [Debugging](#debugging) 26 | 27 | -------------------- 28 | ### Requirements 29 | 30 | - [Erlang](https://github.com/erlang) 31 | - [Elixir](https://elixir-lang.org/) 32 | - [Python](https://www.python.org/downloads/) 33 | 34 | -------------------- 35 | ### Installation 36 | 37 | The following guide provides instructions on how to install both Erlang and 38 | Elixir: https://elixir-lang.org/install.html 39 | 40 | Windows: https://github.com/elixir-lang/elixir/wiki/Windows 41 | 42 | 1. Run `git clone ....` to clone the APXR GitHub repository 43 | 2. Run `cd apxr` to navigate into the main directory 44 | 45 | -------------------- 46 | ### Quick start 47 | 48 | 1. Run `mix check` 49 | 2. Run `bin/run` 50 | 51 | -------------------- 52 | ### Introduction 53 | 54 | An agent-based simulation environment with the goal of being realistic and robust enough for the analysis of algorithmic trading strategies. It is centred around a fully functioning limit order book (LOB) and populations of agents that represent common market behaviours and strategies. The agents operate on different timescales and their strategic behaviour depends on other market participants. 55 | 56 | ABMs can be thought of as models in which a number of heterogeneous agents interact with each other and their environment in a particular way. One of the key advantages of ABMs, compared to the other modelling methods, is their ability to model the heterogeneity of agents. Moreover, ABMs can provide insight into not just the behaviour of individual agents but also the aggregate effects that emerge from the interactions of all agents. This type of modelling lends itself perfectly to capturing the complex phenomena often found in financial systems. 57 | 58 | The main objective of the program is to identify the emerging patterns due to the complex interactions within the market. We consider five categories of traders (simplest explanation of the market ecology) which enables us to credibly mimic (including extreme price changes) price patterns in the market. The model is stated in pseudo-continuous time. That is, a simulated day is divided into T = 300,000 periods (approximately the number of 10ths of a second in an 8.5 h trading day) and during each period there is a possibility for each agent to act. Lower action probabilities correspond to slower trading speeds. 59 | 60 | The model comprises of 5 agent types: Market Makers, Liquidity Consumers, Mean Reversion Traders, Momentum Traders and Noise Traders. Importantly, when chosen, agents are not required to act. This facet allows agents to vary their activity through time and in response the market, as with real-world market participants. 61 | 62 | Upon being chosen to act, if an agent wishes to submit an order, it will communicate an order type, volume and price determined by that agent’s internal logic. The order is then submitted to the LOB where it is matched using price-time priority. If no match occurs then the order is stored in the book until it is later filled or canceled by the originating trader. 63 | 64 | -------------------- 65 | ### Architecture 66 | 67 | The platform favours correctness, developer agility, and stability. Throughput, latency, and execution speed are not overlooked, but viewed as secondary. The idea is to build a system that is simple/correct and then optimise for performance. 68 | 69 | The system is composed of the following Elixir GenServer processes: 70 | - Market: A coordinating process that summons the traders to act on each iteration. 71 | - Exchange: A fully functioning limit order book and matching engine. Provides Level 1 and Level 2 market data. Notifies traders when their trades have executed. 72 | - Traders: Various Trader types that make up the market ecology. 73 | - Reporting service: A service that dispatches public market data to interested parties and writes the varies output series to file. 74 | 75 | Separating the runtime for each of these processes provides us with isolation guarantees that allow us to grow functionality irrespective of dependencies one component may have on another, not to mention the desired behaviour that system failures will not bring down non-dependent parts of the application. Basically, a Trader process failing shouldn't bring down the Exchange. We can think of our system as a collection of small, independent threads of logic that communicate with other processes through an agreed upon interface. 76 | 77 | ![img](https://github.com/Rober-t/apxr/blob/master/img.png) 78 | 79 | **Summary of messages used in the main interactions** 80 | 81 | | Input to venue | Description | Received from | 82 | |------------------------ |--------------------------------------------------------------------- |-------------------------- | 83 | | New Order | A new order is received | Trader | 84 | | Cancel Order | An order cancel request is received | Trader | 85 | 86 | | Output from venue | Description | Sent to | 87 | |------------------------ |--------------------------------------------------------------------- |------------------------- | 88 | | Order Execution Report | An execution report is sent after an order is completed or canceled | Trader | 89 | | Orderbook Event | A report is sent when certain orderbook events occur | Reporting Service | 90 | | Market Data | Level 1 and Level 2 market data | Trader | 91 | 92 | | Input to trader | Description | Received from | 93 | |------------------------ |--------------------------------------------------------------------- |------------------------- | 94 | | Order Execution Report | An execution report is received after an order is completed/canceled | Trading Venue | 95 | | Orderbook Event | If subscribed, a report is sent after certain events occur | Reporting Service | 96 | | Market Data | Level 1 and Level 2 market data | Trading Venue | 97 | 98 | -------------------- 99 | ### Configuration 100 | 101 | Configuration is placed as close as possible to where it is used and not via the application environment. See the module attributes that can be found at the top of most files. 102 | 103 | -------------------- 104 | ### Outputs 105 | 106 | The program outputs four CSV files: 107 | - `apxr_mid_prices` 108 | - `apxr_trades` 109 | - `apxr_order_sides` 110 | - `apxr_price_impacts` 111 | 112 | -------------------- 113 | ### Validation 114 | 115 | The data can be validated with the attached Juypter notebook `validate.ipynb`. It requires Python 3 to be installed. The notebook is configured to import the data from the above files. The model is validated against a number of stylised market properties including: clustered volatility, autocorrelation of returns, long memory in order flow, price impact and the presence of extreme price events. 116 | 117 | Run 118 | 119 | ``` 120 | jupyter notebook 121 | ``` 122 | 123 | Navigate to 'http://localhost:8888/notebooks/validate.ipynb' 124 | 125 | -------------------- 126 | ### Future work 127 | 128 | The program is designed for extension. For example, additional tickers, venues, circuit breakers, etc., can all be added. To implement your own trader see the `MyTrader` module. This can be modified to implement your trading strategy. Multiple different strategies can be added. If you would prefer to work in Python that too can easily be implemented. What you build on top is up to you! 129 | 130 | -------------------- 131 | ### Debugging 132 | 133 | At the top of your module, add the following line: 134 | 135 | ``` 136 | require IEx 137 | ``` 138 | 139 | Next, inside of your function, add the following line: 140 | 141 | ``` 142 | IEx.pry 143 | ``` 144 | 145 | To log something to IO: 146 | 147 | ``` 148 | IO.puts("string") 149 | ``` 150 | 151 | or 152 | 153 | ``` 154 | IO.inspect(SomethingToInspect) 155 | ``` 156 | 157 | -------------------- 158 | 159 | Copyright (C) 2019 ApproximateReality - approximatereality@gmail.com 160 | -------------------------------------------------------------------------------- /bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | mix run -e APXR.Simulation.start --no-halt -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Mix.Config 2 | 3 | config :apxr, 4 | :environment, Mix.env() -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rober-t/apxr/8e720c2df2e5283ec961f5e1f6873c545cc12f92/img.png -------------------------------------------------------------------------------- /lib/apxr/application.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.Application do 2 | @moduledoc """ 3 | See https://hexdocs.pm/elixir/Application.html 4 | for more information on OTP Applications 5 | """ 6 | 7 | use Application 8 | 9 | def start(_type, _args) do 10 | Supervisor.start_link( 11 | [APXR.Simulation, APXR.RunSupervisor], 12 | strategy: :one_for_all, 13 | name: APXR.Supervisor 14 | ) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/apxr/exchange.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.Exchange do 2 | @moduledoc """ 3 | Limit-order-book and order matching engine. 4 | Order matching is the process of accepting buy and sell orders for a 5 | security (or other fungible asset) and matching them to allow trading 6 | between parties who are otherwise unknown to each other. 7 | A LOB is the set of all limit orders for a given asset on a given 8 | platform at time t. 9 | A LOB can be though of as a pair of queues, each of which consists of a 10 | set of buy or sell limit orders at a specified price. 11 | Several different limit orders can reside at the same price at the 12 | same time. LOBs employ a priority system within each individual price level. 13 | Price-time priority: for buy/sell limit orders, priority is given to 14 | the limit orders with the highest/lowest price, and ties are broken by 15 | selecting the limit order with the earliest submission time t. 16 | The actions of traders in an LOB can be expressed solely in terms of 17 | the submission or cancellation of orders. 18 | Two orders are matched when they have a “compatible price” 19 | (i.e. a buy order will match any existing cheaper sell order, 20 | a sell order will match any existing more expensive buy order). 21 | Each matching generates a transaction, giving birth to three messages: 22 | one (public) trade, and two private trades (one to be reported to each owner 23 | of the matched orders). 24 | Members of the trading facility are connected to the server via two channels, 25 | one private channel allowing them to send (and receive) messages to the 26 | server and one public channel: 27 | - The private connection: the trader will send specific orders to the 28 | matching engine and receive answers and messages concerning his orders. 29 | - The public channel, he/she will see (like everyone connected to this 30 | public feed) the transactions and the state of the orderbook. 31 | """ 32 | 33 | use GenServer 34 | 35 | alias APXR.{ 36 | Order, 37 | OrderbookEvent, 38 | ReportingService 39 | } 40 | 41 | @tick_size 0.01 42 | 43 | ## Client API 44 | 45 | @doc """ 46 | Starts the exchange. 47 | """ 48 | def start_link([venue, ticker, init_price, init_vol]) do 49 | name = via_tuple({venue, ticker}) 50 | GenServer.start_link(__MODULE__, [venue, ticker, init_price, init_vol], name: name) 51 | end 52 | 53 | @doc ~S""" 54 | The tick size. 55 | 56 | ## Examples 57 | 58 | iex> APXR.Exchange.tick_size(:apxr, :apxr) 59 | 0.01 60 | 61 | """ 62 | def tick_size(_venue, _ticker) do 63 | @tick_size 64 | end 65 | 66 | @doc ~S""" 67 | Level 1 market data. 68 | The mid_price. 69 | 70 | ## Examples 71 | 72 | iex> APXR.Exchange.mid_price(:apxr, :apxr) 73 | 100.00 74 | 75 | """ 76 | def mid_price(venue, ticker) do 77 | GenServer.call(via_tuple({venue, ticker}), {:mid_price}, 25000) 78 | end 79 | 80 | @doc ~S""" 81 | Level 1 market data. 82 | The highest posted price someone is willing to buy the asset at. 83 | 84 | ## Examples 85 | 86 | iex> APXR.Exchange.bid_price(:apxr, :apxr) 87 | 99.99 88 | 89 | """ 90 | def bid_price(venue, ticker) do 91 | GenServer.call(via_tuple({venue, ticker}), {:bid_price}, 25000) 92 | end 93 | 94 | @doc ~S""" 95 | Level 1 market data. 96 | The volume that people are trying to buy at the bid price. 97 | 98 | ## Examples 99 | 100 | iex> APXR.Exchange.bid_size(:apxr, :apxr) 101 | 100 102 | 103 | """ 104 | def bid_size(venue, ticker) do 105 | GenServer.call(via_tuple({venue, ticker}), {:bid_size}, 25000) 106 | end 107 | 108 | @doc ~S""" 109 | Level 1 market data. 110 | The lowest posted price someone is willing to sell the asset at. 111 | 112 | ## Examples 113 | 114 | iex> APXR.Exchange.ask_price(:apxr, :apxr) 115 | 100.01 116 | 117 | """ 118 | def ask_price(venue, ticker) do 119 | GenServer.call(via_tuple({venue, ticker}), {:ask_price}, 25000) 120 | end 121 | 122 | @doc ~S""" 123 | Level 1 market data. 124 | The volume being sold at the ask price. 125 | 126 | ## Examples 127 | 128 | iex> APXR.Exchange.ask_size(:apxr, :apxr) 129 | 100 130 | 131 | """ 132 | def ask_size(venue, ticker) do 133 | GenServer.call(via_tuple({venue, ticker}), {:ask_size}, 25000) 134 | end 135 | 136 | @doc ~S""" 137 | Level 1 market data. 138 | Returns the price at which the last transaction occurred. 139 | 140 | ## Examples 141 | 142 | #iex> APXR.Exchange.last_price(:apxr, :apxr) 143 | #"75.0" 144 | 145 | """ 146 | def last_price(venue, ticker) do 147 | GenServer.call(via_tuple({venue, ticker}), {:last_price}, 25000) 148 | end 149 | 150 | @doc ~S""" 151 | Level 1 market data. 152 | Returns the number of shares, etc. involved in the last transaction. 153 | 154 | ## Examples 155 | 156 | iex> APXR.Exchange.last_size(:apxr, :apxr) 157 | 1 158 | 159 | """ 160 | def last_size(venue, ticker) do 161 | GenServer.call(via_tuple({venue, ticker}), {:last_size}, 25000) 162 | end 163 | 164 | @doc ~S""" 165 | Level 2 market data. 166 | Returns (up to) the highest 5 prices where traders are willing to buy an asset, 167 | and have placed an order to do so. 168 | 169 | ## Examples 170 | 171 | iex> APXR.Exchange.highest_bid_prices(:apxr, :apxr) 172 | [99.99] 173 | 174 | """ 175 | def highest_bid_prices(venue, ticker) do 176 | GenServer.call(via_tuple({venue, ticker}), {:highest_bid_prices}, 25000) 177 | end 178 | 179 | @doc ~S""" 180 | Level 2 market data. 181 | Returns the volume that people are trying to buy at each of the 182 | highest (up to) 5 prices where traders are willing to buy an asset, 183 | and have placed an order to do so. 184 | 185 | ## Examples 186 | 187 | iex> APXR.Exchange.highest_bid_sizes(:apxr, :apxr) 188 | [100] 189 | 190 | """ 191 | def highest_bid_sizes(venue, ticker) do 192 | GenServer.call(via_tuple({venue, ticker}), {:highest_bid_sizes}, 25000) 193 | end 194 | 195 | @doc ~S""" 196 | Level 2 market data. 197 | Returns (up to) the lowest 5 prices where traders are willing to sell an asset, 198 | and have placed an order to do so. 199 | 200 | ## Examples 201 | 202 | iex> APXR.Exchange.lowest_ask_prices(:apxr, :apxr) 203 | [100.01] 204 | 205 | """ 206 | def lowest_ask_prices(venue, ticker) do 207 | GenServer.call(via_tuple({venue, ticker}), {:lowest_ask_prices}, 25000) 208 | end 209 | 210 | @doc ~S""" 211 | Level 2 market data. 212 | Returns the volume that people are trying to sell at each of the 213 | lowest (up to) 5 prices where traders are willing to sell an asset, 214 | and have placed an order to do so. 215 | 216 | ## Examples 217 | 218 | iex> APXR.Exchange.lowest_ask_sizes(:apxr, :apxr) 219 | [100] 220 | 221 | """ 222 | def lowest_ask_sizes(venue, ticker) do 223 | GenServer.call(via_tuple({venue, ticker}), {:lowest_ask_sizes}, 25000) 224 | end 225 | 226 | @doc """ 227 | Create a buy market order. 228 | 229 | Returns `%Order{} | :rejected`. 230 | """ 231 | def buy_market_order(_venue, _ticker, _trader, vol) when vol <= 0 do 232 | :rejected 233 | end 234 | 235 | def buy_market_order(venue, ticker, trader, vol) do 236 | GenServer.call( 237 | via_tuple({venue, ticker}), 238 | {:market_order, venue, ticker, trader, 0, vol}, 239 | 25000 240 | ) 241 | end 242 | 243 | @doc """ 244 | Create a sell market order. 245 | 246 | Returns `%Order{} | :rejected`. 247 | """ 248 | def sell_market_order(_venue, _ticker, _trader, vol) when vol <= 0 do 249 | :rejected 250 | end 251 | 252 | def sell_market_order(venue, ticker, trader, vol) do 253 | GenServer.call( 254 | via_tuple({venue, ticker}), 255 | {:market_order, venue, ticker, trader, 1, vol}, 256 | 25000 257 | ) 258 | end 259 | 260 | @doc """ 261 | Create a buy limit order. 262 | 263 | Returns `%Order{} | :rejected`. 264 | """ 265 | def buy_limit_order(venue, ticker, trader, price, vol) do 266 | cond do 267 | price <= 0.0 -> 268 | :rejected 269 | 270 | vol <= 0 -> 271 | :rejected 272 | 273 | true -> 274 | GenServer.call( 275 | via_tuple({venue, ticker}), 276 | {:limit_order, venue, ticker, trader, 0, price, vol}, 277 | 25000 278 | ) 279 | end 280 | end 281 | 282 | @doc """ 283 | Create a sell limit order. 284 | 285 | Returns `%Order{} | :rejected`. 286 | """ 287 | def sell_limit_order(venue, ticker, trader, price, vol) do 288 | cond do 289 | price <= 0.0 -> 290 | :rejected 291 | 292 | vol <= 0 -> 293 | :rejected 294 | 295 | true -> 296 | GenServer.call( 297 | via_tuple({venue, ticker}), 298 | {:limit_order, venue, ticker, trader, 1, price, vol}, 299 | 25000 300 | ) 301 | end 302 | end 303 | 304 | @doc """ 305 | Cancel an order. 306 | 307 | Returns `:ok`. 308 | """ 309 | def cancel_order(venue, ticker, order) do 310 | GenServer.call(via_tuple({venue, ticker}), {:cancel_order, order}) 311 | end 312 | 313 | ## Server callbacks 314 | 315 | @impl true 316 | def init([venue, ticker, init_price, init_vol]) do 317 | {:ok, 318 | %{ 319 | venue: venue, 320 | ticker: ticker, 321 | last_price: init_price, 322 | last_size: init_vol, 323 | bid_book: :gb_trees.empty(), 324 | ask_book: :gb_trees.empty() 325 | }} 326 | end 327 | 328 | @impl true 329 | def handle_call({:bid_price}, _from, %{bid_book: bid_book} = state) do 330 | bid_price = do_bid_price(bid_book) 331 | {:reply, bid_price, state} 332 | end 333 | 334 | @impl true 335 | def handle_call({:bid_size}, _from, %{bid_book: bid_book} = state) do 336 | bid_size = do_bid_size(bid_book) 337 | {:reply, bid_size, state} 338 | end 339 | 340 | @impl true 341 | def handle_call({:ask_price}, _from, %{ask_book: ask_book} = state) do 342 | ask_price = do_ask_price(ask_book) 343 | {:reply, ask_price, state} 344 | end 345 | 346 | @impl true 347 | def handle_call({:ask_size}, _from, %{ask_book: ask_book} = state) do 348 | ask_size = do_ask_size(ask_book) 349 | {:reply, ask_size, state} 350 | end 351 | 352 | @impl true 353 | def handle_call({:last_price}, _from, %{last_price: last_price} = state) do 354 | {:reply, last_price, state} 355 | end 356 | 357 | @impl true 358 | def handle_call({:last_size}, _from, %{last_size: last_size} = state) do 359 | {:reply, last_size, state} 360 | end 361 | 362 | @impl true 363 | def handle_call({:mid_price}, _from, %{bid_book: bid_book, ask_book: ask_book} = state) do 364 | mid_price = do_mid_price(bid_book, ask_book) 365 | {:reply, mid_price, state} 366 | end 367 | 368 | @impl true 369 | def handle_call({:highest_bid_prices}, _from, %{bid_book: bid_book} = state) do 370 | highest_bid_prices = do_highest_bid_prices(bid_book) 371 | {:reply, highest_bid_prices, state} 372 | end 373 | 374 | @impl true 375 | def handle_call({:highest_bid_sizes}, _from, %{bid_book: bid_book} = state) do 376 | highest_bid_sizes = do_highest_bid_sizes(bid_book) 377 | {:reply, highest_bid_sizes, state} 378 | end 379 | 380 | @impl true 381 | def handle_call({:lowest_ask_prices}, _from, %{ask_book: ask_book} = state) do 382 | lowest_ask_prices = do_lowest_ask_prices(ask_book) 383 | {:reply, lowest_ask_prices, state} 384 | end 385 | 386 | @impl true 387 | def handle_call({:lowest_ask_sizes}, _from, %{ask_book: ask_book} = state) do 388 | lowest_ask_sizes = do_lowest_ask_sizes(ask_book) 389 | {:reply, lowest_ask_sizes, state} 390 | end 391 | 392 | @impl true 393 | def handle_call({:market_order, venue, ticker, trader, side, vol}, _from, state) do 394 | order = order(venue, ticker, trader, side, :undefined, vol) 395 | {state, order} = do_market_order(order, state) 396 | {:reply, order, state} 397 | end 398 | 399 | @impl true 400 | def handle_call({:limit_order, venue, ticker, trader, side, price, vol}, _from, state) do 401 | order = order(venue, ticker, trader, side, price, vol) 402 | {state, order} = do_limit_order(order, state) 403 | {:reply, order, state} 404 | end 405 | 406 | @impl true 407 | def handle_call({:cancel_order, order}, _from, state) do 408 | state = do_cancel_order(order, state) 409 | {:reply, :ok, state} 410 | end 411 | 412 | @impl true 413 | def handle_info(_msg, state) do 414 | {:noreply, state} 415 | end 416 | 417 | ## Private 418 | 419 | defp via_tuple(id) do 420 | {:via, Registry, {APXR.ExchangeRegistry, id}} 421 | end 422 | 423 | defp do_mid_price(bid_book, ask_book) do 424 | bid_price = do_bid_price(bid_book) 425 | ask_price = do_ask_price(ask_book) 426 | ((bid_price + ask_price) / 2.0) |> Float.round(2) 427 | end 428 | 429 | defp do_bid_price(bid_book) do 430 | if :gb_trees.is_empty(bid_book) do 431 | 0.0 432 | else 433 | {bid_max, _} = :gb_trees.largest(bid_book) 434 | bid_max 435 | end 436 | end 437 | 438 | defp do_bid_size(bid_book) do 439 | if :gb_trees.is_empty(bid_book) do 440 | 0 441 | else 442 | {_bid_max, bid_tree} = :gb_trees.largest(bid_book) 443 | vol = for {_id, order} <- :gb_trees.to_list(bid_tree), do: order.volume 444 | Enum.sum(vol) 445 | end 446 | end 447 | 448 | defp do_ask_price(ask_book) do 449 | if :gb_trees.is_empty(ask_book) do 450 | 0.0 451 | else 452 | {ask_min, _} = :gb_trees.smallest(ask_book) 453 | ask_min 454 | end 455 | end 456 | 457 | defp do_ask_size(ask_book) do 458 | if :gb_trees.is_empty(ask_book) do 459 | 0 460 | else 461 | {_ask_min, ask_tree} = :gb_trees.smallest(ask_book) 462 | vol = for {_id, order} <- :gb_trees.to_list(ask_tree), do: order.volume 463 | Enum.sum(vol) 464 | end 465 | end 466 | 467 | defp do_highest_bid_prices(bid_book) do 468 | bid_book_list = :gb_trees.to_list(bid_book) |> Enum.reverse() |> Enum.slice(0, 5) 469 | for {price, _bid_tree} <- bid_book_list, do: price 470 | end 471 | 472 | defp do_highest_bid_sizes(bid_book) do 473 | bid_book_list = :gb_trees.to_list(bid_book) |> Enum.reverse() |> Enum.slice(0, 5) 474 | bid_tree_list = for {_price, bid_tree} <- bid_book_list, do: bid_tree 475 | 476 | for bid_tree <- bid_tree_list do 477 | vol = for {_id, order} <- :gb_trees.to_list(bid_tree), do: order.volume 478 | Enum.sum(vol) 479 | end 480 | end 481 | 482 | defp do_lowest_ask_prices(ask_book) do 483 | ask_book_list = :gb_trees.to_list(ask_book) |> Enum.slice(0, 5) 484 | for {price, _ask_tree} <- ask_book_list, do: price 485 | end 486 | 487 | defp do_lowest_ask_sizes(ask_book) do 488 | ask_book_list = :gb_trees.to_list(ask_book) |> Enum.slice(0, 5) 489 | ask_tree_list = for {_price, ask_tree} <- ask_book_list, do: ask_tree 490 | 491 | for ask_tree <- ask_tree_list do 492 | vol = for {_id, order} <- :gb_trees.to_list(ask_tree), do: order.volume 493 | Enum.sum(vol) 494 | end 495 | end 496 | 497 | defp order(venue, ticker, trader, side, price, vol) do 498 | %Order{ 499 | venue: venue, 500 | ticker: ticker, 501 | trader_id: trader, 502 | side: side, 503 | price: normalize_price(price), 504 | volume: normalize_volume(vol), 505 | order_id: generate_id() 506 | } 507 | end 508 | 509 | defp do_market_order(%Order{order_id: order_id, side: side} = order, state) do 510 | log_orderbook_event(order, :new_market_order) 511 | ReportingService.push_order_side(timestep(), order_id, :market_order, side) 512 | price_time_match(order, state, :market_order) 513 | end 514 | 515 | defp do_limit_order(%Order{order_id: order_id, side: side} = order, state) do 516 | log_orderbook_event(order, :new_limit_order) 517 | ReportingService.push_order_side(timestep(), order_id, :limit_order, side) 518 | price_time_match(order, state, :limit_order) 519 | end 520 | 521 | defp do_cancel_order( 522 | %Order{order_id: order_id, price: price, side: 0, trader_id: {trader, tid}} = order, 523 | %{bid_book: bid_book} = state 524 | ) do 525 | log_orderbook_event(order, :cancel_limit_order) 526 | 527 | state = 528 | bid_tree(price, bid_book) 529 | |> remove_order_from_tree(order_id) 530 | |> update_bid_book(price, state) 531 | 532 | trader.execution_report({trader, tid}, order, :cancelled_order) 533 | state 534 | end 535 | 536 | defp do_cancel_order( 537 | %Order{order_id: order_id, price: price, side: 1, trader_id: {trader, tid}} = order, 538 | %{ask_book: ask_book} = state 539 | ) do 540 | log_orderbook_event(order, :cancel_limit_order) 541 | 542 | state = 543 | ask_tree(price, ask_book) 544 | |> remove_order_from_tree(order_id) 545 | |> update_ask_book(price, state) 546 | 547 | trader.execution_report({trader, tid}, order, :cancelled_order) 548 | state 549 | end 550 | 551 | defp price_time_match( 552 | %Order{side: 0, volume: vol} = order, 553 | %{ask_book: ask_book} = state, 554 | :market_order 555 | ) do 556 | ask_min = do_ask_price(ask_book) 557 | ask_tree = ask_tree(ask_min, ask_book) 558 | 559 | if :gb_trees.is_empty(ask_tree) do 560 | {state, order} 561 | else 562 | {_key, %Order{volume: matched_vol} = matched_order} = :gb_trees.smallest(ask_tree) 563 | order = %{order | price: ask_min} 564 | 565 | buy_side_match( 566 | matched_vol, 567 | matched_order, 568 | vol, 569 | ask_tree, 570 | state, 571 | ask_min, 572 | order, 573 | :market_order 574 | ) 575 | end 576 | end 577 | 578 | defp price_time_match( 579 | %Order{side: 1, volume: vol} = order, 580 | %{bid_book: bid_book} = state, 581 | :market_order 582 | ) do 583 | bid_max = do_bid_price(bid_book) 584 | bid_tree = bid_tree(bid_max, bid_book) 585 | 586 | if :gb_trees.is_empty(bid_tree) do 587 | {state, order} 588 | else 589 | {_value, %Order{volume: matched_vol} = matched_order} = :gb_trees.smallest(bid_tree) 590 | order = %{order | price: bid_max} 591 | 592 | sell_side_match( 593 | matched_vol, 594 | matched_order, 595 | vol, 596 | bid_tree, 597 | state, 598 | bid_max, 599 | order, 600 | :market_order 601 | ) 602 | end 603 | end 604 | 605 | defp price_time_match( 606 | %Order{side: 0, price: price, volume: vol} = order, 607 | %{ask_book: ask_book} = state, 608 | :limit_order 609 | ) do 610 | if :gb_trees.is_empty(ask_book) do 611 | state = insert_order_into_tree(order, state) 612 | {state, order} 613 | else 614 | {ask_min, _ask_tree} = :gb_trees.smallest(ask_book) 615 | 616 | if price >= ask_min do 617 | check_for_match(vol, state, ask_book, order) 618 | else 619 | state = insert_order_into_tree(order, state) 620 | {state, order} 621 | end 622 | end 623 | end 624 | 625 | defp price_time_match( 626 | %Order{side: 1, price: price, volume: vol} = order, 627 | %{bid_book: bid_book} = state, 628 | :limit_order 629 | ) do 630 | if :gb_trees.is_empty(bid_book) do 631 | state = insert_order_into_tree(order, state) 632 | {state, order} 633 | else 634 | {bid_max, _bid_tree} = :gb_trees.largest(bid_book) 635 | 636 | if price <= bid_max do 637 | check_for_match(vol, state, bid_book, order) 638 | else 639 | state = insert_order_into_tree(order, state) 640 | {state, order} 641 | end 642 | end 643 | end 644 | 645 | defp check_for_match(vol, state, ask_book, %Order{side: 0} = order) do 646 | if :gb_trees.is_empty(ask_book) do 647 | state = insert_order_into_tree(order, state) 648 | {state, order} 649 | else 650 | {ask_min, ask_tree, ask_book2} = :gb_trees.take_smallest(ask_book) 651 | 652 | if :gb_trees.is_empty(ask_tree) do 653 | check_for_match(vol, state, ask_book2, order) 654 | else 655 | {_key, %Order{volume: matched_vol} = matched_order} = :gb_trees.smallest(ask_tree) 656 | 657 | buy_side_match( 658 | matched_vol, 659 | matched_order, 660 | vol, 661 | ask_tree, 662 | state, 663 | ask_min, 664 | order, 665 | :limit_order 666 | ) 667 | end 668 | end 669 | end 670 | 671 | defp check_for_match(vol, state, bid_book, %Order{side: 1} = order) do 672 | if :gb_trees.is_empty(bid_book) do 673 | state = insert_order_into_tree(order, state) 674 | {state, order} 675 | else 676 | {bid_max, bid_tree, bid_book2} = :gb_trees.take_largest(bid_book) 677 | 678 | if :gb_trees.is_empty(bid_tree) do 679 | check_for_match(vol, state, bid_book2, order) 680 | else 681 | {_key, %Order{volume: matched_vol} = matched_order} = :gb_trees.smallest(bid_tree) 682 | 683 | sell_side_match( 684 | matched_vol, 685 | matched_order, 686 | vol, 687 | bid_tree, 688 | state, 689 | bid_max, 690 | order, 691 | :limit_order 692 | ) 693 | end 694 | end 695 | end 696 | 697 | defp buy_side_match( 698 | matched_vol, 699 | _matched_order, 700 | order_vol, 701 | ask_tree, 702 | %{bid_book: bid_book, ask_book: ask_book} = state, 703 | ask_min, 704 | order, 705 | order_type 706 | ) 707 | when matched_vol == order_vol do 708 | {_key, popped_order, ask_tree} = :gb_trees.take_smallest(ask_tree) 709 | mid_price_before = do_mid_price(bid_book, ask_book) 710 | state = update_ask_book(ask_tree, ask_min, state) 711 | %{bid_book: bid_book, ask_book: ask_book} = state 712 | mid_price_after = do_mid_price(bid_book, ask_book) 713 | post_process_buy_order_match(order, popped_order) 714 | 715 | if order_type == :market_order do 716 | ReportingService.push_price_impact( 717 | timestep(), 718 | order.order_id, 719 | :market_order, 720 | order_vol, 721 | mid_price_before, 722 | mid_price_after 723 | ) 724 | end 725 | 726 | state = %{state | last_price: ask_min, last_size: matched_vol} 727 | {state, order} 728 | end 729 | 730 | defp buy_side_match( 731 | matched_vol, 732 | matched_order, 733 | order_vol, 734 | ask_tree, 735 | %{bid_book: bid_book, ask_book: ask_book} = state, 736 | ask_min, 737 | order, 738 | order_type 739 | ) 740 | when matched_vol < order_vol do 741 | {_key, _popped_order, ask_tree} = :gb_trees.take_smallest(ask_tree) 742 | mid_price_before = do_mid_price(bid_book, ask_book) 743 | state = update_ask_book(ask_tree, ask_min, state) 744 | %{bid_book: bid_book, ask_book: ask_book} = state 745 | mid_price_after = do_mid_price(bid_book, ask_book) 746 | post_process_buy_order_match(order, matched_order) 747 | 748 | if order_type == :market_order do 749 | ReportingService.push_price_impact( 750 | timestep(), 751 | order.order_id, 752 | :market_order, 753 | order_vol, 754 | mid_price_before, 755 | mid_price_after 756 | ) 757 | end 758 | 759 | order = %{order | volume: order_vol - matched_vol} 760 | state = %{state | last_price: ask_min, last_size: matched_vol} 761 | price_time_match(order, state, order_type) 762 | end 763 | 764 | defp buy_side_match( 765 | matched_vol, 766 | matched_order, 767 | order_vol, 768 | ask_tree, 769 | %{ask_book: ask_book} = state, 770 | ask_min, 771 | order, 772 | _order_type 773 | ) 774 | when matched_vol > order_vol do 775 | {_key, popped_order, ask_tree} = :gb_trees.take_smallest(ask_tree) 776 | popped_order = %{popped_order | volume: matched_vol - order_vol} 777 | ask_tree = :gb_trees.enter(popped_order.order_id, popped_order, ask_tree) 778 | post_process_buy_order_match(order, matched_order) 779 | ask_book = :gb_trees.enter(ask_min, ask_tree, ask_book) 780 | state = %{state | last_price: ask_min, last_size: order_vol, ask_book: ask_book} 781 | {state, order} 782 | end 783 | 784 | defp sell_side_match( 785 | matched_vol, 786 | _matched_order, 787 | order_vol, 788 | bid_tree, 789 | %{bid_book: bid_book, ask_book: ask_book} = state, 790 | bid_max, 791 | order, 792 | order_type 793 | ) 794 | when matched_vol == order_vol do 795 | {_key, popped_order, bid_tree} = :gb_trees.take_smallest(bid_tree) 796 | mid_price_before = do_mid_price(bid_book, ask_book) 797 | state = update_bid_book(bid_tree, bid_max, state) 798 | %{bid_book: bid_book, ask_book: ask_book} = state 799 | mid_price_after = do_mid_price(bid_book, ask_book) 800 | post_process_sell_order_match(order, popped_order) 801 | 802 | if order_type == :market_order do 803 | ReportingService.push_price_impact( 804 | timestep(), 805 | order.order_id, 806 | :market_order, 807 | order_vol, 808 | mid_price_before, 809 | mid_price_after 810 | ) 811 | end 812 | 813 | state = %{state | last_price: bid_max, last_size: matched_vol} 814 | {state, order} 815 | end 816 | 817 | defp sell_side_match( 818 | matched_vol, 819 | matched_order, 820 | order_vol, 821 | bid_tree, 822 | %{bid_book: bid_book, ask_book: ask_book} = state, 823 | bid_max, 824 | order, 825 | order_type 826 | ) 827 | when matched_vol < order_vol do 828 | {_key, _popped_order, bid_tree} = :gb_trees.take_smallest(bid_tree) 829 | mid_price_before = do_mid_price(bid_book, ask_book) 830 | state = update_bid_book(bid_tree, bid_max, state) 831 | %{bid_book: bid_book, ask_book: ask_book} = state 832 | mid_price_after = do_mid_price(bid_book, ask_book) 833 | post_process_sell_order_match(order, matched_order) 834 | 835 | if order_type == :market_order do 836 | ReportingService.push_price_impact( 837 | timestep(), 838 | order.order_id, 839 | :market_order, 840 | order_vol, 841 | mid_price_before, 842 | mid_price_after 843 | ) 844 | end 845 | 846 | order = %{order | volume: order_vol - matched_vol} 847 | state = %{state | last_price: bid_max, last_size: matched_vol} 848 | price_time_match(order, state, order_type) 849 | end 850 | 851 | defp sell_side_match( 852 | matched_vol, 853 | matched_order, 854 | order_vol, 855 | bid_tree, 856 | %{bid_book: bid_book} = state, 857 | bid_max, 858 | order, 859 | _order_type 860 | ) 861 | when matched_vol > order_vol do 862 | {_key, popped_order, bid_tree} = :gb_trees.take_smallest(bid_tree) 863 | popped_order = %{popped_order | volume: matched_vol - order_vol} 864 | bid_tree = :gb_trees.enter(popped_order.order_id, popped_order, bid_tree) 865 | post_process_sell_order_match(order, matched_order) 866 | bid_book = :gb_trees.enter(bid_max, bid_tree, bid_book) 867 | state = %{state | last_price: bid_max, last_size: order_vol, bid_book: bid_book} 868 | {state, order} 869 | end 870 | 871 | defp post_process_buy_order_match( 872 | %Order{volume: vol, trader_id: {t1, tid1}} = order1, 873 | %Order{volume: matched_vol, price: price, trader_id: {t2, tid2}} = order2 874 | ) 875 | when vol < matched_vol do 876 | t1.execution_report({t1, tid1}, order1, :full_fill_buy_order) 877 | t2.execution_report({t2, tid2}, order2, :partial_fill_buy_order) 878 | log_orderbook_event(order1, vol, price, :full_fill_buy_order, true) 879 | end 880 | 881 | defp post_process_buy_order_match( 882 | %Order{volume: vol, trader_id: {t1, tid1}} = order1, 883 | %Order{volume: matched_vol, price: price, trader_id: {t2, tid2}} = order2 884 | ) 885 | when vol == matched_vol do 886 | t1.execution_report({t1, tid1}, order1, :full_fill_buy_order) 887 | t2.execution_report({t2, tid2}, order2, :full_fill_buy_order) 888 | log_orderbook_event(order1, vol, price, :full_fill_buy_order, true) 889 | end 890 | 891 | defp post_process_buy_order_match( 892 | %Order{volume: vol, trader_id: {t1, tid1}} = order1, 893 | %Order{volume: matched_vol, price: price, trader_id: {t2, tid2}} = order2 894 | ) 895 | when vol > matched_vol do 896 | t1.execution_report({t1, tid1}, order1, :partial_fill_buy_order) 897 | t2.execution_report({t2, tid2}, order2, :full_fill_buy_order) 898 | log_orderbook_event(order1, matched_vol, price, :partial_fill_buy_order, true) 899 | end 900 | 901 | defp post_process_sell_order_match( 902 | %Order{volume: vol, trader_id: {t1, tid1}} = order1, 903 | %Order{volume: matched_vol, price: price, trader_id: {t2, tid2}} = order2 904 | ) 905 | when vol < matched_vol do 906 | t1.execution_report({t1, tid1}, order1, :full_fill_sell_order) 907 | t2.execution_report({t2, tid2}, order2, :full_fill_sell_order) 908 | log_orderbook_event(order1, vol, price, :full_fill_sell_order, true) 909 | end 910 | 911 | defp post_process_sell_order_match( 912 | %Order{volume: vol, trader_id: {t1, tid1}} = order1, 913 | %Order{volume: matched_vol, price: price, trader_id: {t2, tid2}} = order2 914 | ) 915 | when vol == matched_vol do 916 | t1.execution_report({t1, tid1}, order1, :full_fill_sell_order) 917 | t2.execution_report({t2, tid2}, order2, :full_fill_sell_order) 918 | log_orderbook_event(order1, vol, price, :full_fill_sell_order, true) 919 | end 920 | 921 | defp post_process_sell_order_match( 922 | %Order{volume: vol, trader_id: {t1, tid1}} = order1, 923 | %Order{volume: matched_vol, price: price, trader_id: {t2, tid2}} = order2 924 | ) 925 | when vol > matched_vol do 926 | t1.execution_report({t1, tid1}, order1, :partial_fill_sell_order) 927 | t2.execution_report({t2, tid2}, order2, :full_fill_sell_order) 928 | log_orderbook_event(order1, matched_vol, price, :partial_fill_sell_order, true) 929 | end 930 | 931 | defp log_orderbook_event(%Order{} = order, type) do 932 | log_orderbook_event(order, order.volume, order.price, type, false) 933 | end 934 | 935 | defp log_orderbook_event( 936 | %Order{order_id: order_id, trader_id: tid, side: direction}, 937 | size, 938 | price, 939 | type, 940 | transaction 941 | ) 942 | when is_atom(type) and is_boolean(transaction) do 943 | event = %OrderbookEvent{ 944 | timestep: timestep(), 945 | uid: generate_id(), 946 | order_id: order_id, 947 | trader_id: tid, 948 | type: type, 949 | volume: size, 950 | price: price, 951 | direction: direction, 952 | transaction: transaction 953 | } 954 | 955 | ReportingService.push_event(event) 956 | end 957 | 958 | defp bid_tree(bid_max, bid_book) do 959 | case :gb_trees.lookup(bid_max, bid_book) do 960 | {:value, tree} -> 961 | tree 962 | 963 | :none -> 964 | :gb_trees.empty() 965 | end 966 | end 967 | 968 | defp ask_tree(ask_min, ask_book) do 969 | case :gb_trees.lookup(ask_min, ask_book) do 970 | {:value, tree} -> 971 | tree 972 | 973 | :none -> 974 | :gb_trees.empty() 975 | end 976 | end 977 | 978 | defp insert_order_into_tree( 979 | %Order{side: 0, order_id: order_id, price: price} = order, 980 | %{bid_book: bid_book} = state 981 | ) do 982 | tree = bid_tree(price, bid_book) |> tree_insert_order(order_id, order) 983 | bid_book = :gb_trees.enter(price, tree, bid_book) 984 | %{state | bid_book: bid_book} 985 | end 986 | 987 | defp insert_order_into_tree( 988 | %Order{side: 1, order_id: order_id, price: price} = order, 989 | %{ask_book: ask_book} = state 990 | ) do 991 | tree = ask_tree(price, ask_book) |> tree_insert_order(order_id, order) 992 | ask_book = :gb_trees.enter(price, tree, ask_book) 993 | %{state | ask_book: ask_book} 994 | end 995 | 996 | defp tree_insert_order(tree, order_id, order) do 997 | :gb_trees.insert(order_id, order, tree) 998 | end 999 | 1000 | defp remove_order_from_tree(tree, order_id) do 1001 | :gb_trees.delete(order_id, tree) 1002 | rescue 1003 | FunctionClauseError -> 1004 | tree 1005 | end 1006 | 1007 | defp update_bid_book(tree, price, %{bid_book: bid_book} = state) do 1008 | bid_book = 1009 | if :gb_trees.is_empty(tree) do 1010 | :gb_trees.delete(price, bid_book) 1011 | else 1012 | :gb_trees.enter(price, tree, bid_book) 1013 | end 1014 | 1015 | %{state | bid_book: bid_book} 1016 | rescue 1017 | FunctionClauseError -> 1018 | state 1019 | end 1020 | 1021 | defp update_ask_book(tree, price, %{ask_book: ask_book} = state) do 1022 | ask_book = 1023 | if :gb_trees.is_empty(tree) do 1024 | :gb_trees.delete(price, ask_book) 1025 | else 1026 | :gb_trees.enter(price, tree, ask_book) 1027 | end 1028 | 1029 | %{state | ask_book: ask_book} 1030 | rescue 1031 | FunctionClauseError -> 1032 | state 1033 | end 1034 | 1035 | defp normalize_price(:undefined) do 1036 | nil 1037 | end 1038 | 1039 | defp normalize_price(price) when is_integer(price) or is_float(price) do 1040 | (price / 1.0) |> Float.round(2) 1041 | end 1042 | 1043 | def normalize_volume(volume) when is_integer(volume) or is_float(volume) do 1044 | round(volume) 1045 | end 1046 | 1047 | defp generate_id do 1048 | :erlang.unique_integer([:positive, :monotonic]) 1049 | end 1050 | 1051 | defp timestep() do 1052 | [{:step, step}] = :ets.lookup(:timestep, :step) 1053 | step 1054 | end 1055 | end 1056 | -------------------------------------------------------------------------------- /lib/apxr/liquidity_consumer.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.LiquidityConsumer do 2 | @moduledoc """ 3 | Liquidity consumers represent large slower moving funds that make long term 4 | trading decisions based on the re-balancing of portfolios. In real world 5 | markets, these are likely to be large institutional investors. These agents 6 | are either buying or selling a large order of stock over the course of a day 7 | for which they hope to minimize price impact and trading costs. Whether these 8 | agents are buying or selling is assigned with equal probability. 9 | """ 10 | 11 | @behaviour APXR.Trader 12 | 13 | use GenServer 14 | 15 | alias APXR.{ 16 | Exchange, 17 | Trader 18 | } 19 | 20 | @lc_delta 0.1 21 | @lc_max_vol 100_000 22 | 23 | ## Client API 24 | 25 | @doc """ 26 | Starts a LiquidityConsumer trader. 27 | """ 28 | def start_link(id) when is_integer(id) do 29 | name = via_tuple({__MODULE__, id}) 30 | GenServer.start_link(__MODULE__, id, name: name) 31 | end 32 | 33 | @doc """ 34 | Call to action. 35 | 36 | Returns `{:ok, :done}`. 37 | """ 38 | @impl Trader 39 | def actuate(id) do 40 | GenServer.call(via_tuple(id), {:actuate}, 30000) 41 | end 42 | 43 | @doc """ 44 | Update from Exchange concerning an order. 45 | """ 46 | @impl true 47 | def execution_report(id, order, msg) do 48 | GenServer.cast(via_tuple(id), {:execution_report, order, msg}) 49 | end 50 | 51 | ## Server callbacks 52 | 53 | @impl true 54 | def init(id) do 55 | trader = init_trader(id) 56 | side = order_side() 57 | vol = :rand.uniform(@lc_max_vol) 58 | {:ok, %{side: side, vol_to_fill: vol, trader: trader}} 59 | end 60 | 61 | @impl true 62 | def handle_call({:actuate}, _from, state) do 63 | state = action(state) 64 | {:reply, :ok, state} 65 | end 66 | 67 | @impl true 68 | def handle_cast({:execution_report, _order, _msg}, state) do 69 | # Do nothing 70 | {:noreply, state} 71 | end 72 | 73 | @impl true 74 | def handle_info(_msg, state) do 75 | {:noreply, state} 76 | end 77 | 78 | ## Private 79 | 80 | defp via_tuple(id) do 81 | {:via, Registry, {APXR.TraderRegistry, id}} 82 | end 83 | 84 | defp action(%{vol_to_fill: vol, trader: %Trader{}} = state) when vol <= 0 do 85 | state 86 | end 87 | 88 | defp action( 89 | %{side: side, vol_to_fill: vol, trader: %Trader{trader_id: tid, cash: cash} = trader} = 90 | state 91 | ) do 92 | venue = :apxr 93 | ticker = :apxr 94 | 95 | current_vol_avl = vol_avl_opp_best_price(venue, ticker, side) 96 | 97 | if :rand.uniform() < @lc_delta do 98 | cost = place_order(venue, ticker, tid, vol, current_vol_avl, side) 99 | cash = max(cash - cost, 0.0) |> Float.round(2) 100 | trader = %{trader | cash: cash} 101 | state = update_vol_to_fill(vol, current_vol_avl, state) 102 | %{state | trader: trader} 103 | else 104 | update_vol_to_fill(vol, current_vol_avl, state) 105 | end 106 | end 107 | 108 | defp place_order(venue, ticker, tid, vol, vol_avl, :buy) when vol <= vol_avl do 109 | cost = Exchange.ask_price(venue, ticker) * vol 110 | Exchange.buy_market_order(venue, ticker, tid, vol) 111 | cost 112 | end 113 | 114 | defp place_order(venue, ticker, tid, _vol, vol_avl, :buy) do 115 | cost = Exchange.ask_price(venue, ticker) * vol_avl 116 | Exchange.buy_market_order(venue, ticker, tid, vol_avl) 117 | cost 118 | end 119 | 120 | defp place_order(venue, ticker, tid, vol, vol_avl, :sell) when vol <= vol_avl do 121 | cost = Exchange.bid_price(venue, ticker) * vol 122 | Exchange.sell_market_order(venue, ticker, tid, vol) 123 | cost 124 | end 125 | 126 | defp place_order(venue, ticker, tid, _vol, vol_avl, :sell) do 127 | cost = Exchange.bid_price(venue, ticker) * vol_avl 128 | Exchange.sell_market_order(venue, ticker, tid, vol_avl) 129 | cost 130 | end 131 | 132 | defp update_vol_to_fill(vol, vol_avl, state) do 133 | %{state | vol_to_fill: vol - vol_avl} 134 | end 135 | 136 | defp order_side do 137 | if :rand.uniform() < 0.5 do 138 | :buy 139 | else 140 | :sell 141 | end 142 | end 143 | 144 | defp vol_avl_opp_best_price(venue, ticker, :buy) do 145 | Exchange.ask_size(venue, ticker) 146 | end 147 | 148 | defp vol_avl_opp_best_price(venue, ticker, :sell) do 149 | Exchange.bid_size(venue, ticker) 150 | end 151 | 152 | defp init_trader(id) do 153 | %Trader{ 154 | trader_id: {__MODULE__, id}, 155 | type: :liquidity_consumer, 156 | cash: 20_000_000.0, 157 | outstanding_orders: [] 158 | } 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/apxr/market.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.Market do 2 | @moduledoc """ 3 | Coordinates the market participants, for example, summoning the traders to 4 | act each timestep. 5 | """ 6 | 7 | use GenServer 8 | 9 | alias APXR.{ 10 | Exchange, 11 | LiquidityConsumer, 12 | MarketMaker, 13 | MeanReversionTrader, 14 | MomentumTrader, 15 | MyTrader, 16 | NoiseTrader, 17 | ProgressBar, 18 | ReportingService, 19 | Simulation 20 | } 21 | 22 | @timesteps 300_000 23 | 24 | ## Client API 25 | 26 | @doc """ 27 | Starts a Market. 28 | """ 29 | def start_link(opts) when is_list(opts) do 30 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 31 | end 32 | 33 | @doc """ 34 | Triggers a new simulation run. E.g. One day run of the market. 35 | """ 36 | def open(run_number) do 37 | GenServer.cast(__MODULE__, {:open, run_number}) 38 | end 39 | 40 | ## Server callbacks 41 | 42 | @impl true 43 | def init([%{lcs: lcs, mms: mms, mrts: mrts, mmts: mmts, nts: nts, myts: myts}]) do 44 | :ets.new(:timestep, [:public, :named_table, read_concurrency: true]) 45 | traders = init_traders(lcs, mms, mrts, mmts, nts, myts) 46 | {:ok, %{traders: traders}} 47 | end 48 | 49 | @impl true 50 | def handle_cast({:open, run_number}, state) do 51 | do_open(run_number, state) 52 | {:noreply, state} 53 | end 54 | 55 | @impl true 56 | def handle_info(_msg, state) do 57 | {:noreply, state} 58 | end 59 | 60 | ## Private 61 | 62 | defp do_open(run_number, %{traders: traders}) do 63 | ReportingService.prep(run_number) 64 | call_to_action(traders) 65 | end 66 | 67 | defp call_to_action(traders) when is_list(traders) do 68 | IO.puts("\nMARKET OPEN") 69 | :ets.update_counter(:timestep, :step, 1, {0, 0}) 70 | call_to_action(traders, 0, @timesteps) 71 | end 72 | 73 | defp call_to_action(_traders, i, 0) do 74 | ProgressBar.print(i, @timesteps) 75 | IO.puts("\nMARKET CLOSED") 76 | Simulation.run_over() 77 | end 78 | 79 | defp call_to_action(traders, i, timsteps_left) do 80 | if rem(i, 100) == 0, do: ProgressBar.print(i, @timesteps) 81 | 82 | mid_prices = 83 | if Exchange.highest_bid_prices(:apxr, :apxr) == [] or 84 | Exchange.lowest_ask_prices(:apxr, :apxr) == [] do 85 | for {type, id} <- traders do 86 | case type do 87 | NoiseTrader -> 88 | NoiseTrader.actuate({type, id}) 89 | Exchange.mid_price(:apxr, :apxr) 90 | 91 | _ -> 92 | nil 93 | end 94 | end 95 | else 96 | for {type, id} <- traders do 97 | case type do 98 | MarketMaker -> 99 | MarketMaker.actuate({type, id}) 100 | Exchange.mid_price(:apxr, :apxr) 101 | 102 | LiquidityConsumer -> 103 | LiquidityConsumer.actuate({type, id}) 104 | Exchange.mid_price(:apxr, :apxr) 105 | 106 | MomentumTrader -> 107 | MomentumTrader.actuate({type, id}) 108 | Exchange.mid_price(:apxr, :apxr) 109 | 110 | MeanReversionTrader -> 111 | MeanReversionTrader.actuate({type, id}) 112 | Exchange.mid_price(:apxr, :apxr) 113 | 114 | NoiseTrader -> 115 | NoiseTrader.actuate({type, id}) 116 | Exchange.mid_price(:apxr, :apxr) 117 | 118 | MyTrader -> 119 | MyTrader.actuate({type, id}) 120 | Exchange.mid_price(:apxr, :apxr) 121 | end 122 | end 123 | end 124 | 125 | mid_prices = Enum.reject(mid_prices, &is_nil/1) 126 | 127 | (Enum.sum(mid_prices) / length(mid_prices)) 128 | |> Float.round(2) 129 | |> ReportingService.push_mid_price(i + 1) 130 | 131 | :ets.update_counter(:timestep, :step, 1) 132 | Enum.shuffle(traders) |> call_to_action(i + 1, timsteps_left - 1) 133 | end 134 | 135 | defp init_traders(lcs, mms, mrts, mmts, nts, myts) do 136 | lcs = for id <- 1..lcs, do: {APXR.LiquidityConsumer, id} 137 | mms = for id <- 1..mms, do: {APXR.MarketMaker, id} 138 | mrts = for id <- 1..mrts, do: {APXR.MeanReversionTrader, id} 139 | mmts = for id <- 1..mmts, do: {APXR.MomentumTrader, id} 140 | nts = for id <- 1..nts, do: {APXR.NoiseTrader, id} 141 | myts = for id <- 1..myts, do: {APXR.MyTrader, id} 142 | 143 | lcs ++ mms ++ mrts ++ mmts ++ nts ++ myts 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/apxr/market_maker.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.MarketMaker do 2 | @moduledoc """ 3 | Market makers represent market participants who attempt to earn the spread 4 | by supplying liquidity on both sides of the LOB. In traditional markets, 5 | market makers were appointed but in modern electronic exchanges any agent 6 | is able to follow such a strategy. These agents simultaneously post an order 7 | on each side of the book, maintaining an approximately neutral position 8 | throughout the day. They make their income from the difference between 9 | their bids and asks. If one or both limit orders is executed, it will be 10 | replaced by a new one the next time the market maker is chosen to trade. 11 | """ 12 | 13 | @behaviour APXR.Trader 14 | 15 | use GenServer 16 | 17 | alias APXR.{ 18 | Exchange, 19 | Order, 20 | OrderbookEvent, 21 | Trader 22 | } 23 | 24 | @mm_delta 0.1 25 | @mm_w 50 26 | @mm_max_vol 200_000 27 | @mm_vol 1 28 | 29 | ## Client API 30 | 31 | @doc """ 32 | Starts a MarketMaker trader. 33 | """ 34 | def start_link(id) when is_integer(id) do 35 | name = via_tuple({__MODULE__, id}) 36 | GenServer.start_link(__MODULE__, id, name: name) 37 | end 38 | 39 | @doc """ 40 | Call to action. 41 | 42 | Returns `{:ok, :done}`. 43 | """ 44 | @impl Trader 45 | def actuate(id) do 46 | GenServer.call(via_tuple(id), {:actuate}, 30000) 47 | end 48 | 49 | @doc """ 50 | Update from Exchange concerning an order. 51 | """ 52 | @impl true 53 | def execution_report(id, order, msg) do 54 | GenServer.cast(via_tuple(id), {:execution_report, order, msg}) 55 | end 56 | 57 | ## Server callbacks 58 | 59 | @impl true 60 | def init(id) do 61 | {:ok, _} = Registry.register(APXR.ReportingServiceRegistry, "orderbook_event", []) 62 | trader = init_trader(id) 63 | init_side = Enum.random(0..1) 64 | prediction = :rand.uniform() 65 | {:ok, %{order_side_history: [init_side], prediction: prediction, trader: trader}} 66 | end 67 | 68 | @impl true 69 | def handle_call({:actuate}, _from, state) do 70 | state = action(state) 71 | {:reply, :ok, state} 72 | end 73 | 74 | @impl true 75 | def handle_cast({:broadcast, %OrderbookEvent{type: :new_market_order} = event}, state) do 76 | state = update_prediction(event, state) 77 | {:noreply, state} 78 | end 79 | 80 | @impl true 81 | def handle_cast({:broadcast, %OrderbookEvent{type: :new_limit_order} = event}, state) do 82 | state = update_prediction(event, state) 83 | {:noreply, state} 84 | end 85 | 86 | @impl true 87 | def handle_cast({:execution_report, order, msg}, state) do 88 | state = update_outstanding_orders(order, state, msg) 89 | {:noreply, state} 90 | end 91 | 92 | @impl true 93 | def handle_info(_msg, state) do 94 | {:noreply, state} 95 | end 96 | 97 | ## Private 98 | 99 | defp via_tuple(id) do 100 | {:via, Registry, {APXR.TraderRegistry, id}} 101 | end 102 | 103 | defp update_outstanding_orders( 104 | %Order{order_id: order_id}, 105 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 106 | msg 107 | ) 108 | when msg in [:full_fill_buy_order, :full_fill_sell_order] do 109 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 110 | trader = %{trader | outstanding_orders: outstanding} 111 | %{state | trader: trader} 112 | end 113 | 114 | defp update_outstanding_orders( 115 | %Order{order_id: order_id} = order, 116 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 117 | msg 118 | ) 119 | when msg in [:partial_fill_buy_order, :partial_fill_sell_order] do 120 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 121 | trader = %{trader | outstanding_orders: [order | outstanding]} 122 | %{state | trader: trader} 123 | end 124 | 125 | defp update_outstanding_orders( 126 | %Order{order_id: order_id}, 127 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 128 | :cancelled_order 129 | ) do 130 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 131 | trader = %{trader | outstanding_orders: outstanding} 132 | %{state | trader: trader} 133 | end 134 | 135 | defp action( 136 | %{ 137 | prediction: prediction, 138 | trader: %Trader{trader_id: tid, cash: cash, outstanding_orders: outstanding} = trader 139 | } = state 140 | ) do 141 | venue = :apxr 142 | ticker = :apxr 143 | bid = Exchange.bid_price(venue, ticker) 144 | ask = Exchange.ask_price(venue, ticker) 145 | 146 | if :rand.uniform() < @mm_delta do 147 | for order <- outstanding, do: Exchange.cancel_order(venue, ticker, order) 148 | {cost, orders} = place_order(venue, ticker, tid, ask, bid, prediction) 149 | cash = max(cash - cost, 0.0) |> Float.round(2) 150 | trader = %{trader | cash: cash, outstanding_orders: orders} 151 | %{state | trader: trader} 152 | else 153 | state 154 | end 155 | end 156 | 157 | defp place_order(venue, ticker, tid, ask_price, bid_price, prediction) do 158 | if prediction < 0.5 do 159 | place_order(venue, ticker, tid, ask_price, bid_price, prediction, :lt) 160 | else 161 | place_order(venue, ticker, tid, ask_price, bid_price, prediction, :gt) 162 | end 163 | end 164 | 165 | defp place_order(venue, ticker, tid, ask_price, bid_price, _prediction, :lt) do 166 | vol = :rand.uniform(@mm_max_vol) 167 | order1 = Exchange.sell_limit_order(venue, ticker, tid, ask_price, vol) 168 | order2 = Exchange.buy_limit_order(venue, ticker, tid, bid_price, @mm_vol) 169 | orders = Enum.reject([order1, order2], fn order -> order == :rejected end) 170 | cost = ask_price * vol + bid_price * @mm_vol 171 | {cost, orders} 172 | end 173 | 174 | defp place_order(venue, ticker, tid, ask_price, bid_price, _prediction, :gt) do 175 | vol = :rand.uniform(@mm_max_vol) 176 | order1 = Exchange.buy_limit_order(venue, ticker, tid, bid_price, vol) 177 | order2 = Exchange.sell_limit_order(venue, ticker, tid, ask_price, @mm_vol) 178 | orders = Enum.reject([order1, order2], fn order -> order == :rejected end) 179 | cost = ask_price * @mm_vol + bid_price * vol 180 | {cost, orders} 181 | end 182 | 183 | defp update_prediction(%OrderbookEvent{direction: side}, %{order_side_history: osh} = state) do 184 | osh = order_side_history(side, osh) 185 | prediction = simple_moving_avg(osh) 186 | %{state | order_side_history: osh, prediction: prediction} 187 | end 188 | 189 | defp order_side_history(side, order_side_history) do 190 | if length(order_side_history) < @mm_w do 191 | [side | order_side_history] 192 | else 193 | order_side_history = Enum.drop(order_side_history, -1) 194 | [side | order_side_history] 195 | end 196 | end 197 | 198 | defp simple_moving_avg(items) when is_list(items) do 199 | sum = Enum.reduce(items, 0, fn x, acc -> x + acc end) 200 | sum / length(items) 201 | end 202 | 203 | defp init_trader(id) do 204 | %Trader{ 205 | trader_id: {__MODULE__, id}, 206 | type: :market_maker, 207 | cash: 20_000_000.0, 208 | outstanding_orders: [] 209 | } 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/apxr/mean_reversion_trader.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.MeanReversionTrader do 2 | @moduledoc """ 3 | Mean reversion traders believe that asset prices tend to revert towards their 4 | historical average (though this may be a very short term average). They 5 | attempt to generate profit by taking long positions when the market price 6 | is below the historical average price, and short positions when it is above. 7 | Specifically, we define agents that, when chosen to trade, compare the 8 | current price to an exponential moving average of the asset price 9 | """ 10 | 11 | @behaviour APXR.Trader 12 | 13 | use GenServer 14 | 15 | alias APXR.{ 16 | Exchange, 17 | OrderbookEvent, 18 | Trader 19 | } 20 | 21 | @tick_size Exchange.tick_size(:apxr, :apxr) 22 | 23 | @mrt_delta 0.4 24 | @mrt_vol 1 25 | @mrt_k 1 26 | @mrt_a 0.94 27 | @mrt_n 50 28 | 29 | ## Client API 30 | 31 | @doc """ 32 | Starts a MeanReversionTrader trader. 33 | """ 34 | def start_link(id) when is_integer(id) do 35 | name = via_tuple({__MODULE__, id}) 36 | GenServer.start_link(__MODULE__, id, name: name) 37 | end 38 | 39 | @doc """ 40 | Call to action. 41 | 42 | Returns `{:ok, :done}`. 43 | """ 44 | @impl Trader 45 | def actuate(id) do 46 | GenServer.call(via_tuple(id), {:actuate}, 30000) 47 | end 48 | 49 | @doc """ 50 | Update from Exchange concerning an order. 51 | """ 52 | @impl true 53 | def execution_report(id, order, msg) do 54 | GenServer.cast(via_tuple(id), {:execution_report, order, msg}) 55 | end 56 | 57 | ## Server callbacks 58 | 59 | @impl true 60 | def init(id) do 61 | {:ok, _} = Registry.register(APXR.ReportingServiceRegistry, "orderbook_event", []) 62 | trader = init_trader(id) 63 | price = Exchange.last_price(:apxr, :apxr) 64 | {:ok, %{std_dev: 0.0, ema: nil, price_history: [price], trader: trader}} 65 | end 66 | 67 | @impl true 68 | def handle_call({:actuate}, _from, state) do 69 | state = action(state) 70 | {:reply, :ok, state} 71 | end 72 | 73 | @impl true 74 | def handle_cast( 75 | {:broadcast, %OrderbookEvent{price: price, transaction: true, type: type}}, 76 | state 77 | ) 78 | when type in [ 79 | :full_fill_buy_order, 80 | :full_fill_sell_order, 81 | :partial_fill_buy_order, 82 | :partial_fill_sell_order 83 | ] do 84 | state = update_stats(price, state) 85 | {:noreply, state} 86 | end 87 | 88 | @impl true 89 | def handle_cast({:execution_report, _order, _msg}, state) do 90 | # Do nothing 91 | {:noreply, state} 92 | end 93 | 94 | @impl true 95 | def handle_info(_msg, state) do 96 | {:noreply, state} 97 | end 98 | 99 | ## Private 100 | 101 | defp via_tuple(id) do 102 | {:via, Registry, {APXR.TraderRegistry, id}} 103 | end 104 | 105 | defp action(%{ema: nil} = state) do 106 | state 107 | end 108 | 109 | defp action( 110 | %{ema: ema, std_dev: std_dev, trader: %Trader{trader_id: tid, cash: cash} = trader} = 111 | state 112 | ) do 113 | venue = :apxr 114 | ticker = :apxr 115 | bid_price = Exchange.bid_price(venue, ticker) 116 | ask_price = Exchange.ask_price(venue, ticker) 117 | price = Exchange.last_price(venue, ticker) 118 | 119 | if :rand.uniform() < @mrt_delta do 120 | cost = place_order(venue, ticker, tid, ask_price, bid_price, price, ema, std_dev) 121 | cash = max(cash - cost, 0.0) |> Float.round(2) 122 | trader = %{trader | cash: cash} 123 | %{state | trader: trader} 124 | else 125 | state 126 | end 127 | end 128 | 129 | defp place_order(venue, ticker, tid, ask_price, bid_price, p, ema, std_dev) do 130 | cond do 131 | p - ema >= @mrt_k * std_dev -> 132 | price = ask_price - @tick_size 133 | Exchange.sell_limit_order(venue, ticker, tid, price, @mrt_vol) 134 | price * @mrt_vol 135 | 136 | ema - p >= @mrt_k * std_dev -> 137 | price = bid_price + @tick_size 138 | Exchange.buy_limit_order(venue, ticker, tid, price, @mrt_vol) 139 | price * @mrt_vol 140 | 141 | true -> 142 | 0.0 143 | end 144 | end 145 | 146 | defp update_stats(price, %{price_history: price_history} = state) do 147 | n = length(price_history) 148 | prev_ema = exponential_moving_avg(n, price_history) 149 | ema = prev_ema + @mrt_a * (price - prev_ema) 150 | std_dev = std_dev(price_history) 151 | price_history = price_history(price, price_history) 152 | %{state | std_dev: std_dev, ema: ema, price_history: price_history} 153 | end 154 | 155 | defp price_history(price, price_history) do 156 | if length(price_history) < @mrt_n do 157 | [price | price_history] 158 | else 159 | price_history = Enum.drop(price_history, -1) 160 | [price | price_history] 161 | end 162 | end 163 | 164 | defp std_dev(samples) do 165 | total = Enum.sum(samples) 166 | sample_size = length(samples) 167 | average = total / sample_size 168 | variance = variance(samples, average, sample_size) 169 | :math.sqrt(variance) 170 | end 171 | 172 | defp variance(samples, average, sample_size) do 173 | total_variance = 174 | Enum.reduce(samples, 0, fn sample, total -> 175 | total + :math.pow(sample - average, 2) 176 | end) 177 | 178 | total_variance / (sample_size - 1) 179 | end 180 | 181 | # Source: https://github.com/jhartwell/Taex - MIT License 182 | defp exponential_moving_avg(n, prices) do 183 | [head | _] = exp_calc(n, prices) 184 | head 185 | end 186 | 187 | defp exp_calc(k, [head | tail]), do: exp_calc(k, tail, [head]) 188 | defp exp_calc(_, [], emas), do: emas 189 | 190 | defp exp_calc(n, [p | tail], [ema_head | ema_tail]) do 191 | k = weighting_multiplier(n) 192 | exp_calc(n, tail, [p * k + ema_head * (1 - k)] ++ [ema_head] ++ ema_tail) 193 | end 194 | 195 | defp weighting_multiplier(n) do 196 | 2 / (n + 1) 197 | end 198 | 199 | defp init_trader(id) do 200 | %Trader{ 201 | trader_id: {__MODULE__, id}, 202 | type: :mean_reversion_trader, 203 | cash: 20_000_000.0, 204 | outstanding_orders: [] 205 | } 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/apxr/momentum_trader.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.MomentumTrader do 2 | @moduledoc """ 3 | Momentum traders invest based on the belief that price changes have inertia 4 | a strategy known to be widely used. A momentum strategy involves taking a 5 | long position when prices have been recently rising, and a short position 6 | when they have recently been falling. Specifically, we implement simple 7 | momentum trading agents that rely on calculating a rate of change (ROC) 8 | to detect momentum. 9 | """ 10 | 11 | @behaviour APXR.Trader 12 | 13 | use GenServer 14 | 15 | alias APXR.{ 16 | Exchange, 17 | OrderbookEvent, 18 | Trader 19 | } 20 | 21 | @mt_delta 0.4 22 | @mt_n 5 23 | @mt_k 0.001 24 | 25 | ## Client API 26 | 27 | @doc """ 28 | Starts a MomentumTrader trader. 29 | """ 30 | def start_link(id) when is_integer(id) do 31 | name = via_tuple({__MODULE__, id}) 32 | GenServer.start_link(__MODULE__, id, name: name) 33 | end 34 | 35 | @doc """ 36 | Call to action. 37 | 38 | Returns `{:ok, :done}`. 39 | """ 40 | @impl Trader 41 | def actuate(id) do 42 | GenServer.call(via_tuple(id), {:actuate}, 30000) 43 | end 44 | 45 | @doc """ 46 | Update from Exchange concerning an order. 47 | """ 48 | @impl true 49 | def execution_report(id, order, msg) do 50 | GenServer.cast(via_tuple(id), {:execution_report, order, msg}) 51 | end 52 | 53 | ## Server callbacks 54 | 55 | @impl true 56 | def init(id) do 57 | {:ok, _} = Registry.register(APXR.ReportingServiceRegistry, "orderbook_event", []) 58 | trader = init_trader(id) 59 | price = Exchange.last_price(:apxr, :apxr) 60 | {:ok, %{price_history: [price], roc: 0.0, trader: trader}} 61 | end 62 | 63 | @impl true 64 | def handle_call({:actuate}, _from, state) do 65 | state = action(state) 66 | {:reply, :ok, state} 67 | end 68 | 69 | @impl true 70 | def handle_cast( 71 | {:broadcast, %OrderbookEvent{price: price, transaction: true, type: type}}, 72 | state 73 | ) 74 | when type in [ 75 | :full_fill_buy_order, 76 | :full_fill_sell_order, 77 | :partial_fill_buy_order, 78 | :partial_fill_sell_order 79 | ] do 80 | state = update_roc(price, state) 81 | {:noreply, state} 82 | end 83 | 84 | @impl true 85 | def handle_cast({:execution_report, _order, _msg}, state) do 86 | # Do nothing 87 | {:noreply, state} 88 | end 89 | 90 | @impl true 91 | def handle_info(_msg, state) do 92 | {:noreply, state} 93 | end 94 | 95 | ## Private 96 | 97 | defp via_tuple(id) do 98 | {:via, Registry, {APXR.TraderRegistry, id}} 99 | end 100 | 101 | defp action(%{trader: %Trader{cash: 0.0}} = state) do 102 | state 103 | end 104 | 105 | defp action(%{roc: roc, trader: %Trader{trader_id: tid, cash: cash} = trader} = state) do 106 | venue = :apxr 107 | ticker = :apxr 108 | vol = round(roc * cash) 109 | 110 | if :rand.uniform() < @mt_delta do 111 | cost = place_order(venue, ticker, tid, vol, roc) 112 | cash = max(cash - cost, 0.0) |> Float.round(2) 113 | trader = %{trader | cash: cash} 114 | %{state | trader: trader} 115 | else 116 | state 117 | end 118 | end 119 | 120 | defp place_order(venue, ticker, tid, vol, roc) do 121 | cond do 122 | roc >= @mt_k -> 123 | place_order(venue, ticker, tid, vol, roc, :gt) 124 | 125 | roc <= @mt_k * -1 -> 126 | place_order(venue, ticker, tid, vol, roc, :lt) 127 | 128 | true -> 129 | 0.0 130 | end 131 | end 132 | 133 | defp place_order(venue, ticker, tid, vol, _roc, :gt) do 134 | cost = Exchange.ask_price(venue, ticker) * vol 135 | Exchange.buy_market_order(venue, ticker, tid, vol) 136 | cost 137 | end 138 | 139 | defp place_order(venue, ticker, tid, vol, _roc, :lt) do 140 | cost = Exchange.bid_price(venue, ticker) * vol 141 | Exchange.sell_market_order(venue, ticker, tid, vol) 142 | cost 143 | end 144 | 145 | defp update_roc(price, %{price_history: price_history} = state) do 146 | [price_prev] = Enum.take(price_history, -1) 147 | roc = rate_of_change(price, price_prev) 148 | price_history = price_history(price, price_history) 149 | %{state | roc: roc, price_history: price_history} 150 | end 151 | 152 | defp price_history(price, price_history) do 153 | if length(price_history) < @mt_n do 154 | [price | price_history] 155 | else 156 | price_history = Enum.drop(price_history, -1) 157 | [price | price_history] 158 | end 159 | end 160 | 161 | defp rate_of_change(price, prive_prev) do 162 | abs((price - prive_prev) / prive_prev) 163 | end 164 | 165 | defp init_trader(id) do 166 | %Trader{ 167 | trader_id: {__MODULE__, id}, 168 | type: :momentum_trader, 169 | cash: 20_000_000.0, 170 | outstanding_orders: [] 171 | } 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /lib/apxr/my_trader.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.MyTrader do 2 | @moduledoc """ 3 | Implement your trading strategy using this module as a template or modify it 4 | directly. 5 | """ 6 | 7 | @behaviour APXR.Trader 8 | 9 | use GenServer 10 | 11 | alias APXR.{ 12 | Order, 13 | Trader 14 | } 15 | 16 | ## Client API 17 | 18 | @doc """ 19 | Starts a MyTrader trader. 20 | """ 21 | def start_link(id) when is_integer(id) do 22 | name = via_tuple({__MODULE__, id}) 23 | GenServer.start_link(__MODULE__, id, name: name) 24 | end 25 | 26 | @doc """ 27 | Call to action. 28 | 29 | Returns `{:ok, :done}`. 30 | """ 31 | @impl Trader 32 | def actuate(id) do 33 | GenServer.call(via_tuple(id), {:actuate}, 30000) 34 | end 35 | 36 | @doc """ 37 | Update from Exchange concerning an order. 38 | """ 39 | @impl true 40 | def execution_report(id, order, msg) do 41 | GenServer.cast(via_tuple(id), {:execution_report, order, msg}) 42 | end 43 | 44 | ## Server callbacks 45 | 46 | @impl true 47 | def init(id) do 48 | {:ok, _} = Registry.register(APXR.ReportingServiceRegistry, "orderbook_event", []) 49 | trader = init_trader(id) 50 | {:ok, %{order_side_history: [], trader: trader}} 51 | end 52 | 53 | @impl true 54 | def handle_call({:actuate}, _from, state) do 55 | state = action(state) 56 | {:reply, :ok, state} 57 | end 58 | 59 | @impl true 60 | def handle_cast({:broadcast, _event}, state) do 61 | # Do nothing 62 | {:noreply, state} 63 | end 64 | 65 | @impl true 66 | def handle_cast({:execution_report, order, msg}, state) do 67 | state = update_outstanding_orders(order, state, msg) 68 | {:noreply, state} 69 | end 70 | 71 | @impl true 72 | def handle_info(_msg, state) do 73 | {:noreply, state} 74 | end 75 | 76 | ## Private 77 | 78 | defp via_tuple(id) do 79 | {:via, Registry, {APXR.TraderRegistry, id}} 80 | end 81 | 82 | defp update_outstanding_orders( 83 | %Order{order_id: order_id}, 84 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 85 | msg 86 | ) 87 | when msg in [:full_fill_buy_order, :full_fill_sell_order] do 88 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 89 | trader = %{trader | outstanding_orders: outstanding} 90 | %{state | trader: trader} 91 | end 92 | 93 | defp update_outstanding_orders( 94 | %Order{order_id: order_id} = order, 95 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 96 | msg 97 | ) 98 | when msg in [:partial_fill_buy_order, :partial_fill_sell_order] do 99 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 100 | trader = %{trader | outstanding_orders: [order | outstanding]} 101 | %{state | trader: trader} 102 | end 103 | 104 | defp update_outstanding_orders( 105 | %Order{order_id: order_id}, 106 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 107 | :cancelled_order 108 | ) do 109 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 110 | trader = %{trader | outstanding_orders: outstanding} 111 | %{state | trader: trader} 112 | end 113 | 114 | defp action(%{trader: %Trader{} = _trader} = state) do 115 | # Your logic goes here... 116 | state 117 | end 118 | 119 | defp init_trader(id) do 120 | %Trader{ 121 | trader_id: {__MODULE__, id}, 122 | type: :my_trader, 123 | cash: 20_000_000.0, 124 | outstanding_orders: [] 125 | } 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/apxr/nimble_csv.ex: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Plataformatec 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | defmodule NimbleCSV do 16 | @moduledoc ~S""" 17 | NimbleCSV is a small and fast parsing and dumping library. 18 | 19 | It works by building highly-inlined CSV parsers, designed 20 | to work with strings, enumerables and streams. At the top 21 | of your file (and not inside a function), you can define your 22 | own parser module: 23 | 24 | NimbleCSV.define(MyParser, separator: "\t", escape: "\"") 25 | 26 | Once defined, we can parse data accordingly: 27 | 28 | iex> MyParser.parse_string "name\tage\njohn\t27" 29 | [["john","27"]] 30 | 31 | See the `define/2` function for the list of functions that 32 | would be defined in `MyParser`. 33 | 34 | ## Parsing 35 | 36 | NimbleCSV is by definition restricted in scope to do only 37 | parsing (and dumping). For example, the example above 38 | discarded the headers when parsing the string, as NimbleCSV 39 | expects developers to handle those explicitly later. 40 | For example: 41 | 42 | "name\tage\njohn\t27" 43 | |> MyParser.parse_string 44 | |> Enum.map(fn [name, age] -> 45 | %{name: name, age: String.to_integer(age)} 46 | end) 47 | 48 | This is particularly useful with the parse_stream functionality 49 | that receives and returns a stream. For example, we can use it 50 | to parse files line by line lazily: 51 | 52 | "path/to/csv/file" 53 | |> File.stream!(read_ahead: 100_000) 54 | |> MyParser.parse_stream 55 | |> Stream.map(fn [name, age] -> 56 | %{name: name, age: String.to_integer(age)} 57 | end) 58 | 59 | By default this library ships with `NimbleCSV.RFC4180`, which 60 | is the most common implementation of CSV parsing/dumping available 61 | using comma as separators and double-quote as escape. If you 62 | want to use it in your codebase, simply alias it to CSV and enjoy: 63 | 64 | iex> alias NimbleCSV.RFC4180, as: CSV 65 | iex> CSV.parse_string "name,age\njohn,27" 66 | [["john","27"]] 67 | 68 | ### Binary references 69 | 70 | One of the reasons behind NimbleCSV performance is that it performs 71 | parsing by matching on binaries and extracting those fields as 72 | binary references. Therefore if you have a row such as: 73 | 74 | one,two,three,four,five 75 | 76 | NimbleCSV will return a list of `["one", "two", "three", "four", "five"]` 77 | where each element references the original row. For this reason, if 78 | you plan to keep the parsed data around in the parsing process or even 79 | send it to another process, you may want to copy the data before doing 80 | the transfer. 81 | 82 | For example, in the `parse_stream` example in the previous section, 83 | we could rewrite the `Stream.map/2` operation to explicitly copy any 84 | field that is stored as a binary: 85 | 86 | "path/to/csv/file" 87 | |> File.stream!(read_ahead: 100_000) 88 | |> MyParser.parse_stream 89 | |> Stream.map(fn [name, age] -> 90 | %{name: :binary.copy(name), 91 | age: String.to_integer(age)} 92 | end) 93 | 94 | ## Dumping 95 | 96 | NimbleCSV can dump any enumerable to either iodata or to streams: 97 | 98 | iex> IO.iodata_to_binary MyParser.dump_to_iodata([~w(name age), ~w(mary 28)]) 99 | "name\tage\nmary\t28\n" 100 | 101 | iex> MyParser.dump_to_stream([~w(name age), ~w(mary 28)]) 102 | #Stream<...> 103 | 104 | """ 105 | 106 | defmodule ParseError do 107 | defexception [:message] 108 | end 109 | 110 | @doc """ 111 | Eagerly dumps an enumerable into iodata (a list of binaries and bytes and other lists). 112 | """ 113 | @callback dump_to_iodata(rows :: Enumerable.t()) :: iodata() 114 | 115 | @doc """ 116 | Lazily dumps from an enumerable to a stream. 117 | 118 | It returns a stream that emits each row as iodata. 119 | """ 120 | @callback dump_to_stream(rows :: Enumerable.t()) :: Enumerable.t() 121 | 122 | @doc """ 123 | Same as `parse_enumerable(enumerable, [])`. 124 | """ 125 | @callback parse_enumerable(enum :: Enumerable.t()) :: [[binary()]] 126 | 127 | @doc """ 128 | Eagerly parses CSV from an enumerable and returns a list of rows. 129 | 130 | ## Options 131 | 132 | * `:skip_headers` - when `true`, skips headers. Defaults to `true`. 133 | Set it to false to keep headers or when the CSV has no headers. 134 | 135 | """ 136 | @callback parse_enumerable(enum :: Enumerable.t(), opts :: keyword()) :: [[binary()]] 137 | 138 | @doc """ 139 | Same as `parse_stream(enumerable, [])`. 140 | """ 141 | @callback parse_stream(enum :: Enumerable.t()) :: Enumerable.t() 142 | 143 | @doc """ 144 | Lazily parses CSV from a stream and returns a stream of rows. 145 | 146 | ## Options 147 | 148 | * `:skip_headers` - when `true`, skips headers. Defaults to `true`. 149 | Set it to false to keep headers or when the CSV has no headers. 150 | 151 | """ 152 | @callback parse_stream(enum :: Enumerable.t(), opts :: keyword()) :: Enumerable.t() 153 | 154 | @doc """ 155 | Same as `parse_string(enumerable, [])`. 156 | """ 157 | @callback parse_string(binary()) :: [[binary()]] 158 | 159 | @doc """ 160 | Eagerly parses CSV from a string and returns a list of rows. 161 | 162 | ## Options 163 | 164 | * `:skip_headers` - when `true`, skips headers. Defaults to `true`. 165 | Set it to false to keep headers or when the CSV has no headers. 166 | 167 | """ 168 | @callback parse_string(binary(), opts :: keyword()) :: [[binary()]] 169 | 170 | @doc ~S""" 171 | Defines a new parser/dumper. 172 | 173 | Calling this function defines a CSV module. Therefore, `define` 174 | is typically invoked at the top of your files and not inside 175 | functions. Placing it inside a function would cause the same 176 | module to be defined multiple times, one time per invocation, 177 | leading your code to emit warnings and slowing down execution. 178 | 179 | It accepts the following options: 180 | 181 | * `:moduledoc` - the documentation for the generated module 182 | 183 | The following options control parsing: 184 | 185 | * `:escape`- the CSV escape, defaults to `"\""` 186 | * `:separator`- the CSV separators, defaults to `","`. It can be 187 | a string or a list of strings. If a list is given, the first entry 188 | is used for dumping (see below) 189 | * `:newlines` - the list of entries to be considered newlines 190 | when parsing, defaults to `["\r\n", "\n"]` (note they are attempted 191 | in order, so the order matters) 192 | 193 | The following options control dumping: 194 | 195 | * `:escape`- the CSV escape character, defaults to `"\""` 196 | * `:separator`- the CSV separator character, defaults to `","` 197 | * `:line_separator` - the CSV line separator character, defaults to `"\n"` 198 | * `:reserved` - the list of characters to be escaped, it defaults to the 199 | `:separator`, `:line_separator` and `:escape` characters above. 200 | 201 | Although parsing may support multiple newline delimiters, when 202 | dumping only one of them must be picked, which is controlled by 203 | the `:line_separator` option. This allows NimbleCSV to handle both 204 | `"\r\n"` and `"\n"` when parsing, but only the latter for dumping. 205 | 206 | ## Parser/Dumper API 207 | 208 | Modules defined with `define/2` implement the `NimbleCSV` behaviour. See 209 | the callbacks for this behaviour for information on the generated functions 210 | and their documentation. 211 | """ 212 | def define(module, options) do 213 | defmodule module do 214 | @moduledoc Keyword.get(options, :moduledoc) 215 | @escape Keyword.get(options, :escape, "\"") 216 | @separator (case Keyword.get(options, :separator, ",") do 217 | many when is_list(many) -> many 218 | one when is_binary(one) -> [one] 219 | end) 220 | @line_separator Keyword.get(options, :line_separator, "\n") 221 | @newlines Keyword.get(options, :newlines, ["\r\n", "\n"]) 222 | @reserved Keyword.get(options, :reserved, [@escape, @line_separator | @separator]) 223 | 224 | @behaviour NimbleCSV 225 | 226 | ## Parser 227 | 228 | def parse_stream(stream, opts \\ []) when is_list(opts) do 229 | {state, separator, escape} = init_parser(opts) 230 | 231 | Stream.transform( 232 | stream, 233 | fn -> state end, 234 | &parse(&1, &2, separator, escape), 235 | &finalize_parser/1 236 | ) 237 | end 238 | 239 | def parse_enumerable(enumerable, opts \\ []) when is_list(opts) do 240 | {state, separator, escape} = init_parser(opts) 241 | 242 | {lines, state} = 243 | Enum.flat_map_reduce(enumerable, state, &parse(&1, &2, separator, escape)) 244 | 245 | finalize_parser(state) 246 | lines 247 | end 248 | 249 | def parse_string(string, opts \\ []) when is_binary(string) and is_list(opts) do 250 | newline = :binary.compile_pattern(@newlines) 251 | 252 | {0, byte_size(string)} 253 | |> Stream.unfold(fn 254 | {_, 0} -> 255 | nil 256 | 257 | {offset, length} -> 258 | case :binary.match(string, newline, scope: {offset, length}) do 259 | {newline_offset, newline_length} -> 260 | difference = newline_length + newline_offset - offset 261 | 262 | {binary_part(string, offset, difference), 263 | {newline_offset + newline_length, length - difference}} 264 | 265 | :nomatch -> 266 | {binary_part(string, offset, length), {offset + length, 0}} 267 | end 268 | end) 269 | |> parse_enumerable(opts) 270 | end 271 | 272 | defp init_parser(opts) do 273 | state = 274 | if Keyword.has_key?(opts, :headers) do 275 | IO.warn("the :headers option is deprecated, please use :skip_headers instead") 276 | if Keyword.get(opts, :headers, true), do: :header, else: :line 277 | else 278 | if Keyword.get(opts, :skip_headers, true), do: :header, else: :line 279 | end 280 | 281 | {state, :binary.compile_pattern(@separator), :binary.compile_pattern(@escape)} 282 | end 283 | 284 | defp finalize_parser({:escape, _, _, _}) do 285 | raise ParseError, "expected escape character #{@escape} but reached the end of file" 286 | end 287 | 288 | defp finalize_parser(_) do 289 | :ok 290 | end 291 | 292 | defp to_enum(result) do 293 | case result do 294 | {:line, row} -> {[row], :line} 295 | {:header, _} -> {[], :line} 296 | {:escape, _, _, _} = escape -> {[], escape} 297 | end 298 | end 299 | 300 | defp parse(line, {:escape, entry, row, state}, separator, escape) do 301 | to_enum(escape(line, entry, row, state, separator, escape)) 302 | end 303 | 304 | defp parse(line, state, separator, escape) do 305 | to_enum(separator(line, [], state, separator, escape)) 306 | end 307 | 308 | defmacrop newlines_separator!() do 309 | newlines_offsets = 310 | for {newline, i} <- Enum.with_index(@newlines) do 311 | quote do 312 | unquote(Macro.var(:"count#{i}", Elixir)) = offset - unquote(byte_size(newline)) 313 | end 314 | end 315 | 316 | newlines_clauses = 317 | @newlines 318 | |> Enum.with_index() 319 | |> Enum.flat_map(fn {newline, i} -> 320 | quote do 321 | <> -> 322 | prefix 323 | end 324 | end) 325 | |> Kernel.++(quote do: (prefix -> prefix)) 326 | 327 | quote do 328 | offset = byte_size(var!(line)) 329 | unquote(newlines_offsets) 330 | case var!(line), do: unquote(newlines_clauses) 331 | end 332 | end 333 | 334 | defmacrop separator_case() do 335 | clauses = 336 | Enum.flat_map(@separator, fn sep -> 337 | quote do 338 | <> -> 339 | escape( 340 | rest, 341 | "", 342 | var!(row) ++ :binary.split(prefix, var!(separator), [:global]), 343 | var!(state), 344 | var!(separator), 345 | var!(escape) 346 | ) 347 | end 348 | end) 349 | 350 | catch_all = 351 | quote do 352 | _ -> 353 | raise ParseError, 354 | "unexpected escape character #{@escape} in #{inspect(var!(line))}" 355 | end 356 | 357 | quote do 358 | case var!(line) do 359 | unquote(clauses ++ catch_all) 360 | end 361 | end 362 | end 363 | 364 | defp separator(line, row, state, separator, escape) do 365 | case :binary.match(line, escape) do 366 | {0, _} -> 367 | <<@escape, rest::binary>> = line 368 | escape(rest, "", row, state, separator, escape) 369 | 370 | {pos, _} -> 371 | pos = pos - 1 372 | separator_case() 373 | 374 | :nomatch -> 375 | pruned = newlines_separator!() 376 | {state, row ++ :binary.split(pruned, separator, [:global])} 377 | end 378 | end 379 | 380 | defmacrop newlines_escape!(match) do 381 | newlines_before = 382 | quote do 383 | <> -> 384 | escape( 385 | rest, 386 | var!(entry) <> prefix <> <<@escape>>, 387 | var!(row), 388 | var!(state), 389 | var!(separator), 390 | var!(escape) 391 | ) 392 | end ++ 393 | Enum.flat_map(@separator, fn sep -> 394 | quote do 395 | <> -> 396 | separator( 397 | rest, 398 | var!(row) ++ [var!(entry) <> prefix], 399 | var!(state), 400 | var!(separator), 401 | var!(escape) 402 | ) 403 | end 404 | end) 405 | 406 | newlines_clauses = 407 | Enum.flat_map(@newlines, fn newline -> 408 | quote do 409 | <> -> 410 | {var!(state), var!(row) ++ [var!(entry) <> prefix]} 411 | end 412 | end) 413 | 414 | newlines_after = 415 | quote do 416 | <> -> 417 | {var!(state), var!(row) ++ [var!(entry) <> prefix]} 418 | 419 | _ -> 420 | raise ParseError, "unexpected escape character #{@escape} in #{inspect(var!(line))}" 421 | end 422 | 423 | quote do 424 | case unquote(match) do 425 | {offset, _} -> 426 | case var!(line), do: unquote(newlines_before ++ newlines_clauses ++ newlines_after) 427 | 428 | :nomatch -> 429 | {:escape, var!(entry) <> var!(line), var!(row), var!(state)} 430 | end 431 | end 432 | end 433 | 434 | defp escape(line, entry, row, state, separator, escape) do 435 | newlines_escape!(:binary.match(line, escape)) 436 | end 437 | 438 | @compile {:inline, init_parser: 1, to_enum: 1, parse: 4} 439 | 440 | ## Dumper 441 | 442 | def dump_to_iodata(enumerable) do 443 | check = init_dumper() 444 | Enum.map(enumerable, &dump(&1, check)) 445 | end 446 | 447 | def dump_to_stream(enumerable) do 448 | check = init_dumper() 449 | Stream.map(enumerable, &dump(&1, check)) 450 | end 451 | 452 | @escape_minimum (case @escape do 453 | <> -> x 454 | x -> x 455 | end) 456 | 457 | @separator_minimum (case @separator do 458 | [<> | _] -> x 459 | [x | _] -> x 460 | end) 461 | 462 | @line_separator_minimum (case @line_separator do 463 | <> -> x 464 | x -> x 465 | end) 466 | 467 | @replacement @escape <> @escape 468 | 469 | defp init_dumper() do 470 | :binary.compile_pattern(@reserved) 471 | end 472 | 473 | defp dump([], _check) do 474 | [@line_separator_minimum] 475 | end 476 | 477 | defp dump([entry], check) do 478 | [maybe_escape(entry, check), @line_separator_minimum] 479 | end 480 | 481 | defp dump([entry | entries], check) do 482 | [maybe_escape(entry, check), @separator_minimum | dump(entries, check)] 483 | end 484 | 485 | defp maybe_escape(entry, check) do 486 | entry = to_string(entry) 487 | 488 | case :binary.match(entry, check) do 489 | {_, _} -> 490 | replaced = :binary.replace(entry, @escape, @replacement, [:global]) 491 | [@escape_minimum, replaced, @escape_minimum] 492 | 493 | :nomatch -> 494 | entry 495 | end 496 | end 497 | 498 | @compile {:inline, init_dumper: 0, maybe_escape: 2} 499 | end 500 | end 501 | end 502 | 503 | NimbleCSV.define(NimbleCSV.RFC4180, 504 | separator: ",", 505 | escape: "\"", 506 | moduledoc: """ 507 | A CSV parser that uses comma as separator and double-quotes as escape according to RFC4180. 508 | """ 509 | ) 510 | -------------------------------------------------------------------------------- /lib/apxr/noise_trader.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.NoiseTrader do 2 | @moduledoc """ 3 | Noise traders defined so as to capture all other market activity. The noise 4 | traders are randomly assigned whether to submit a buy or sell order in each 5 | period with equal probability. Once assigned, they then randomly place either 6 | a market or limit order or cancel an existing order. To prevent spurious 7 | price processes, noise traders market orders are limited in volume such that 8 | they cannot consume more than half of the total opposing side’s available 9 | volume. Another restriction is that noise traders will make sure that no side 10 | of the order book is empty and place limit orders appropriately. 11 | """ 12 | 13 | @behaviour APXR.Trader 14 | 15 | use GenServer 16 | 17 | alias APXR.{ 18 | Exchange, 19 | Order, 20 | Trader 21 | } 22 | 23 | @tick_size Exchange.tick_size(:apxr, :apxr) 24 | 25 | @default_spread 0.05 26 | @default_price 100 27 | 28 | @nt_delta 0.75 29 | @nt_m 0.03 30 | @nt_l 0.54 31 | @nt_mu_mo 7 32 | @nt_mu_lo 8 33 | @nt_sigma_mo 0.1 34 | @nt_sigma_lo 0.7 35 | @nt_crs 0.003 36 | @nt_inspr 0.098 37 | @nt_spr 0.173 38 | @nt_xmin 0.005 39 | @nt_beta 2.72 40 | 41 | ## Client API 42 | 43 | @doc """ 44 | Starts a NoiseTrader trader. 45 | """ 46 | def start_link(id) when is_integer(id) do 47 | name = via_tuple({__MODULE__, id}) 48 | GenServer.start_link(__MODULE__, id, name: name) 49 | end 50 | 51 | @doc """ 52 | Call to action. 53 | 54 | Returns `{:ok, :done}`. 55 | """ 56 | @impl Trader 57 | def actuate(id) do 58 | GenServer.call(via_tuple(id), {:actuate}, 30000) 59 | end 60 | 61 | @doc """ 62 | Update from Exchange concerning an order. 63 | """ 64 | @impl true 65 | def execution_report(id, order, msg) do 66 | GenServer.cast(via_tuple(id), {:execution_report, order, msg}) 67 | end 68 | 69 | ## Server callbacks 70 | 71 | @impl true 72 | def init(id) do 73 | trader = init_trader(id) 74 | {:ok, %{trader: trader}} 75 | end 76 | 77 | @impl true 78 | def handle_call({:actuate}, _from, state) do 79 | state = action(state) 80 | {:reply, :ok, state} 81 | end 82 | 83 | @impl true 84 | def handle_cast({:execution_report, order, msg}, state) do 85 | state = update_outstanding_orders(order, state, msg) 86 | {:noreply, state} 87 | end 88 | 89 | @impl true 90 | def handle_info(_msg, state) do 91 | {:noreply, state} 92 | end 93 | 94 | ## Private 95 | 96 | defp via_tuple(id) do 97 | {:via, Registry, {APXR.TraderRegistry, id}} 98 | end 99 | 100 | defp update_outstanding_orders( 101 | %Order{order_id: order_id}, 102 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 103 | msg 104 | ) 105 | when msg in [:full_fill_buy_order, :full_fill_sell_order] do 106 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 107 | trader = %{trader | outstanding_orders: outstanding} 108 | %{state | trader: trader} 109 | end 110 | 111 | defp update_outstanding_orders( 112 | %Order{order_id: order_id} = order, 113 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 114 | msg 115 | ) 116 | when msg in [:partial_fill_buy_order, :partial_fill_sell_order] do 117 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 118 | trader = %{trader | outstanding_orders: [order | outstanding]} 119 | %{state | trader: trader} 120 | end 121 | 122 | defp update_outstanding_orders( 123 | %Order{order_id: order_id}, 124 | %{trader: %Trader{outstanding_orders: outstanding} = trader} = state, 125 | :cancelled_order 126 | ) do 127 | outstanding = Enum.reject(outstanding, fn %Order{order_id: id} -> id == order_id end) 128 | trader = %{trader | outstanding_orders: outstanding} 129 | %{state | trader: trader} 130 | end 131 | 132 | defp action( 133 | %{trader: %Trader{trader_id: tid, cash: cash, outstanding_orders: outstanding} = trader} = 134 | state 135 | ) do 136 | venue = :apxr 137 | ticker = :apxr 138 | type = order_side() 139 | bid_price = Exchange.bid_price(venue, ticker) 140 | ask_price = Exchange.ask_price(venue, ticker) 141 | spread = max(ask_price - bid_price, @tick_size) 142 | off_sprd_amnt = off_sprd_amnt(@nt_xmin, @nt_beta) + spread 143 | in_spr_price = Enum.random(round(bid_price * 100)..round(ask_price * 100)) / 100 144 | 145 | if Exchange.highest_bid_prices(venue, ticker) == [] or 146 | Exchange.lowest_ask_prices(venue, ticker) == [] do 147 | {cost, orders} = populate_orderbook(venue, ticker, tid, bid_price, ask_price) 148 | cash = max(cash - cost, 0.0) |> Float.round(2) 149 | orders = Enum.reject(orders, fn order -> order == :rejected end) 150 | trader = %{trader | cash: cash, outstanding_orders: outstanding ++ orders} 151 | %{state | trader: trader} 152 | else 153 | if :rand.uniform() < @nt_delta do 154 | case :rand.uniform() do 155 | action when action < @nt_m -> 156 | cost = market_order(venue, ticker, type, tid) 157 | cash = max(cash - cost, 0.0) |> Float.round(2) 158 | trader = %{trader | cash: cash} 159 | %{state | trader: trader} 160 | 161 | action when action < @nt_m + @nt_l -> 162 | {cost, orders} = 163 | case :rand.uniform() do 164 | lo when lo < @nt_crs -> 165 | limit_order(type, venue, ticker, tid, ask_price, bid_price) 166 | 167 | lo when lo < @nt_crs + @nt_inspr -> 168 | limit_order(type, venue, ticker, tid, in_spr_price, in_spr_price) 169 | 170 | lo when lo < @nt_crs + @nt_inspr + @nt_spr -> 171 | limit_order(type, venue, ticker, tid, bid_price, ask_price) 172 | 173 | _ -> 174 | limit_order(type, venue, ticker, tid, bid_price, ask_price, off_sprd_amnt) 175 | end 176 | 177 | cash = max(cash - cost, 0.0) |> Float.round(2) 178 | orders = Enum.reject(orders, fn order -> order == :rejected end) 179 | trader = %{trader | cash: cash, outstanding_orders: outstanding ++ orders} 180 | %{state | trader: trader} 181 | 182 | _ -> 183 | outstanding = maybe_cancel_order(venue, ticker, outstanding) 184 | trader = %{trader | outstanding_orders: outstanding} 185 | %{state | trader: trader} 186 | end 187 | else 188 | state 189 | end 190 | end 191 | end 192 | 193 | defp maybe_cancel_order(venue, ticker, orders) when is_list(orders) and length(orders) > 0 do 194 | {orders, [order]} = Enum.split(orders, -1) 195 | Exchange.cancel_order(venue, ticker, order) 196 | orders 197 | end 198 | 199 | defp maybe_cancel_order(_venue, _ticker, orders) do 200 | orders 201 | end 202 | 203 | defp market_order(venue, ticker, :buy, tid) do 204 | vol = 205 | min( 206 | Enum.sum(Exchange.lowest_ask_prices(venue, ticker)), 207 | :math.exp(@nt_mu_mo + @nt_sigma_mo * :rand.uniform()) 208 | ) 209 | 210 | Exchange.buy_market_order(venue, ticker, tid, vol) 211 | vol * Exchange.ask_price(venue, ticker) 212 | end 213 | 214 | defp market_order(venue, ticker, :sell, tid) do 215 | vol = 216 | min( 217 | Enum.sum(Exchange.highest_bid_prices(venue, ticker)), 218 | :math.exp(@nt_mu_mo + @nt_sigma_mo * :rand.uniform()) 219 | ) 220 | 221 | Exchange.sell_market_order(venue, ticker, tid, vol) 222 | vol * Exchange.bid_price(venue, ticker) 223 | end 224 | 225 | defp limit_order(:buy, venue, ticker, tid, price1, _price2) do 226 | vol = limit_order_vol() 227 | order = Exchange.buy_limit_order(venue, ticker, tid, price1, vol) 228 | cost = vol * price1 229 | {cost, [order]} 230 | end 231 | 232 | defp limit_order(:sell, venue, ticker, tid, _price1, price2) do 233 | vol = limit_order_vol() 234 | order = Exchange.sell_limit_order(venue, ticker, tid, price2, vol) 235 | cost = vol * price2 236 | {cost, [order]} 237 | end 238 | 239 | defp limit_order(:buy, venue, ticker, tid, bid_price, _ask_price, off_sprd_amnt) do 240 | vol = limit_order_vol() 241 | price = bid_price + off_sprd_amnt 242 | order = Exchange.buy_limit_order(venue, ticker, tid, price, vol) 243 | cost = vol * price 244 | {cost, [order]} 245 | end 246 | 247 | defp limit_order(:sell, venue, ticker, tid, _bid_price, ask_price, off_sprd_amnt) do 248 | vol = limit_order_vol() 249 | price = ask_price - off_sprd_amnt 250 | order = Exchange.sell_limit_order(venue, ticker, tid, price, vol) 251 | cost = vol * price 252 | {cost, [order]} 253 | end 254 | 255 | defp limit_order_vol() do 256 | :math.exp(@nt_mu_lo + @nt_sigma_lo * :rand.uniform()) |> round() 257 | end 258 | 259 | defp off_sprd_amnt(xmin, beta) do 260 | pow = 1 / (beta - 1) * -1 261 | num = 1 - :rand.uniform() 262 | xmin * :math.pow(num, pow) 263 | end 264 | 265 | defp order_side do 266 | if :rand.uniform() < 0.5 do 267 | :buy 268 | else 269 | :sell 270 | end 271 | end 272 | 273 | defp populate_orderbook(venue, ticker, tid, bid_price, ask_price) do 274 | cond do 275 | Exchange.highest_bid_prices(venue, ticker) == [] and 276 | Exchange.lowest_ask_prices(venue, ticker) == [] -> 277 | limit_order(:buy, venue, ticker, tid, @default_price, @default_price) 278 | limit_order(:sell, venue, ticker, tid, @default_price, @default_price + @default_spread) 279 | 280 | Exchange.highest_bid_prices(venue, ticker) == [] -> 281 | limit_order(:buy, venue, ticker, tid, ask_price - @default_spread, @default_price) 282 | 283 | Exchange.lowest_ask_prices(venue, ticker) == [] -> 284 | limit_order(:sell, venue, ticker, tid, @default_price, bid_price + @default_spread) 285 | 286 | true -> 287 | :ok 288 | end 289 | end 290 | 291 | defp init_trader(id) do 292 | %Trader{ 293 | trader_id: {__MODULE__, id}, 294 | type: :noise_trader, 295 | cash: 20_000_000.0, 296 | outstanding_orders: [] 297 | } 298 | end 299 | end 300 | -------------------------------------------------------------------------------- /lib/apxr/order.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.Order do 2 | @moduledoc """ 3 | Represents an order in the system. 4 | """ 5 | 6 | @enforce_keys [ 7 | :ticker, 8 | :venue, 9 | :order_id, 10 | :trader_id, 11 | :side, 12 | :volume 13 | ] 14 | 15 | defstruct ticker: nil, 16 | venue: nil, 17 | order_id: nil, 18 | trader_id: nil, 19 | side: nil, 20 | volume: nil, 21 | price: nil 22 | 23 | @type t() :: %__MODULE__{ 24 | ticker: atom(), 25 | venue: atom(), 26 | order_id: pos_integer(), 27 | trader_id: tuple(), 28 | side: non_neg_integer(), 29 | volume: pos_integer(), 30 | price: float() | nil 31 | } 32 | end 33 | -------------------------------------------------------------------------------- /lib/apxr/orderbook_event.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.OrderbookEvent do 2 | @moduledoc """ 3 | Represents an orderbook event in the system. 4 | """ 5 | 6 | @enforce_keys [ 7 | :timestep, 8 | :uid, 9 | :type, 10 | :order_id, 11 | :trader_id, 12 | :volume, 13 | :direction, 14 | :transaction 15 | ] 16 | 17 | defstruct timestep: nil, 18 | uid: nil, 19 | type: nil, 20 | order_id: nil, 21 | trader_id: nil, 22 | volume: nil, 23 | price: nil, 24 | direction: nil, 25 | transaction: nil 26 | 27 | @type t() :: %__MODULE__{ 28 | timestep: pos_integer(), 29 | uid: integer(), 30 | type: atom(), 31 | order_id: pos_integer(), 32 | trader_id: tuple(), 33 | volume: pos_integer(), 34 | price: float() | nil, 35 | direction: integer(), 36 | transaction: boolean() 37 | } 38 | end 39 | -------------------------------------------------------------------------------- /lib/apxr/progress_bar.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.ProgressBar do 2 | @moduledoc """ 3 | Command-line progress bar 4 | """ 5 | 6 | @progress_bar_size 50 7 | @complete_character "=" 8 | @incomplete_character "_" 9 | 10 | def print(current, total) do 11 | percent = percent = (current / total * 100) |> Float.round(1) 12 | divisor = 100 / @progress_bar_size 13 | 14 | complete_count = round(percent / divisor) 15 | incomplete_count = @progress_bar_size - complete_count 16 | 17 | complete = String.duplicate(@complete_character, complete_count) 18 | incomplete = String.duplicate(@incomplete_character, incomplete_count) 19 | 20 | progress_bar = "|#{complete}#{incomplete}| #{percent}% #{current}/#{total}" 21 | 22 | IO.write("\r#{progress_bar}") 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/apxr/reporting_service.ex: -------------------------------------------------------------------------------- 1 | NimbleCSV.define(CSV.RFC4180, 2 | separator: ",", 3 | escape: "\"", 4 | skip_headers: true, 5 | moduledoc: """ 6 | A CSV parser that uses comma as separator and double-quotes as escape 7 | according to RFC4180. 8 | """ 9 | ) 10 | 11 | defmodule APXR.ReportingService do 12 | @moduledoc """ 13 | Trade reporting service. For example, dispatches events to subscribed agents. 14 | """ 15 | 16 | use GenServer 17 | 18 | alias APXR.OrderbookEvent 19 | alias CSV.RFC4180, as: CSV 20 | 21 | ## Client API 22 | 23 | @doc """ 24 | Starts ReportingService. 25 | """ 26 | def start_link(opts) when is_list(opts) do 27 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 28 | end 29 | 30 | @doc """ 31 | Prep the Reporting Service. 32 | """ 33 | def prep(run_number) do 34 | GenServer.cast(__MODULE__, {:prep, run_number}) 35 | end 36 | 37 | @doc """ 38 | Writes the mid-price to disk for later processing and analysis. 39 | """ 40 | def push_mid_price(price, timestep) do 41 | GenServer.cast(__MODULE__, {:push_mid_price, timestep, price}) 42 | end 43 | 44 | @doc """ 45 | Writes the order side to disk for later processing and analysis. 46 | """ 47 | def push_order_side(timestep, id, type, side) do 48 | GenServer.cast(__MODULE__, {:push_order_side, timestep, id, type, side}) 49 | end 50 | 51 | @doc """ 52 | Writes the price impact data to disk for later processing and analysis. 53 | """ 54 | def push_price_impact(timestep, id, type, volume, before_p, after_p) do 55 | GenServer.cast( 56 | __MODULE__, 57 | {:push_price_impact, timestep, id, type, volume, before_p, after_p} 58 | ) 59 | end 60 | 61 | @doc """ 62 | Writes the order book events to disk for later processing and analysis and 63 | dispatches them to subscribed agents. 64 | """ 65 | def push_event(%OrderbookEvent{} = event) do 66 | GenServer.cast(__MODULE__, {:push_event, event}) 67 | end 68 | 69 | ## Server callbacks 70 | 71 | @impl true 72 | def init(_opts) do 73 | {:ok, %{}} 74 | end 75 | 76 | @impl true 77 | def handle_cast({:prep, run_number}, _state) do 78 | state = do_prep(run_number) 79 | {:noreply, state} 80 | end 81 | 82 | @impl true 83 | def handle_cast({:push_mid_price, _timestep, price}, state) do 84 | write_csv_file([[price]], :mid_price, state) 85 | {:noreply, state} 86 | end 87 | 88 | @impl true 89 | def handle_cast({:push_order_side, _timestep, _order_id, _order_type, side}, state) do 90 | write_csv_file([[side]], :order_side, state) 91 | {:noreply, state} 92 | end 93 | 94 | @impl true 95 | def handle_cast({:push_price_impact, _timestep, _id, _type, vol, before_p, after_p}, state) do 96 | impact = impact(before_p, after_p) 97 | write_csv_file([[vol, impact]], :price_impact, state) 98 | {:noreply, state} 99 | end 100 | 101 | @impl true 102 | def handle_cast({:push_event, event}, state) do 103 | process_event(event, state) 104 | {:noreply, state} 105 | end 106 | 107 | @impl true 108 | def handle_info(_msg, state) do 109 | {:noreply, state} 110 | end 111 | 112 | @impl true 113 | def terminate(_reason, %{ 114 | event_device: ed, 115 | mid_price_device: mpd, 116 | order_side_device: osd, 117 | price_impact_device: pimpd 118 | }) do 119 | File.close(ed) 120 | File.close(mpd) 121 | File.close(osd) 122 | File.close(pimpd) 123 | end 124 | 125 | ## Private 126 | 127 | defp do_prep(run_number) do 128 | run = to_string(run_number) 129 | 130 | event_log_path = File.cwd!() |> Path.join("/output/apxr_trades" <> run <> ".csv") 131 | ed = File.open!(event_log_path, [:delayed_write, :append]) 132 | 133 | mid_price_path = File.cwd!() |> Path.join("/output/apxr_mid_prices" <> run <> ".csv") 134 | mpd = File.open!(mid_price_path, [:delayed_write, :append]) 135 | 136 | order_side_path = File.cwd!() |> Path.join("/output/apxr_order_sides" <> run <> ".csv") 137 | osd = File.open!(order_side_path, [:delayed_write, :append]) 138 | 139 | price_impact_path = File.cwd!() |> Path.join("/output/apxr_price_impacts" <> run <> ".csv") 140 | pimpd = File.open!(price_impact_path, [:delayed_write, :append]) 141 | 142 | %{ 143 | run_number: run_number, 144 | event_device: ed, 145 | mid_price_device: mpd, 146 | order_side_device: osd, 147 | price_impact_device: pimpd 148 | } 149 | end 150 | 151 | defp process_event(%OrderbookEvent{transaction: true} = event, state) do 152 | write_csv_file(event, :event, state) 153 | broadcast_event(event) 154 | end 155 | 156 | defp process_event(%OrderbookEvent{transaction: false} = event, _state) do 157 | broadcast_event(event) 158 | end 159 | 160 | defp broadcast_event(%OrderbookEvent{} = event) do 161 | Registry.dispatch(APXR.ReportingServiceRegistry, "orderbook_event", fn entries -> 162 | for {pid, _} <- entries, do: send(pid, {:broadcast, event}) 163 | end) 164 | end 165 | 166 | defp write_csv_file(row, :mid_price, %{mid_price_device: device}) do 167 | if Application.get_env(:apxr, :environment) == :test do 168 | :ok 169 | else 170 | data = CSV.dump_to_iodata(row) 171 | IO.binwrite(device, data) 172 | end 173 | end 174 | 175 | defp write_csv_file(row, :order_side, %{order_side_device: device}) do 176 | if Application.get_env(:apxr, :environment) == :test do 177 | :ok 178 | else 179 | data = CSV.dump_to_iodata(row) 180 | IO.binwrite(device, data) 181 | end 182 | end 183 | 184 | defp write_csv_file(row, :price_impact, %{price_impact_device: device}) do 185 | if Application.get_env(:apxr, :environment) == :test do 186 | :ok 187 | else 188 | data = CSV.dump_to_iodata(row) 189 | IO.binwrite(device, data) 190 | end 191 | end 192 | 193 | defp write_csv_file(row, :event, %{event_device: device, run_number: run_number}) do 194 | if Application.get_env(:apxr, :environment) == :test do 195 | :ok 196 | else 197 | data = parse_event_data(run_number, row) |> CSV.dump_to_iodata() 198 | IO.binwrite(device, data) 199 | end 200 | end 201 | 202 | defp parse_event_data(_run_number, %OrderbookEvent{price: price}) do 203 | [[price]] 204 | end 205 | 206 | defp impact(before_p, after_p) do 207 | before_p = max(before_p, 0.0001) 208 | after_p = max(after_p, 0.0001) 209 | :math.log(after_p) - :math.log(before_p) 210 | end 211 | end 212 | -------------------------------------------------------------------------------- /lib/apxr/run_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.RunSupervisor do 2 | # See https://hexdocs.pm/elixir/Supervisor.html 3 | # for other strategies and supported options 4 | 5 | @moduledoc false 6 | 7 | use Supervisor 8 | 9 | @init_price 100 10 | @init_vol 1 11 | 12 | # How many of each type of trader to initialize 13 | @liquidity_consumers 10 14 | @market_makers 10 15 | @mean_reversion_traders 40 16 | @momentum_traders 40 17 | @noise_traders 75 18 | @my_traders 1 19 | 20 | def start_link(opts) do 21 | Supervisor.start_link(__MODULE__, opts, name: __MODULE__) 22 | end 23 | 24 | @impl true 25 | @spec init(any()) :: no_return() 26 | def init(_) do 27 | # List all child processes to be supervised 28 | children = [ 29 | Supervisor.child_spec( 30 | {Registry, 31 | [ 32 | keys: :duplicate, 33 | name: APXR.ReportingServiceRegistry, 34 | partitions: System.schedulers_online() 35 | ]}, 36 | id: :reporting_service 37 | ), 38 | Supervisor.child_spec( 39 | {Registry, 40 | [ 41 | keys: :unique, 42 | name: APXR.ExchangeRegistry, 43 | partitions: System.schedulers_online() 44 | ]}, 45 | id: :exchange_registry 46 | ), 47 | Supervisor.child_spec( 48 | {Registry, 49 | [ 50 | keys: :unique, 51 | name: APXR.TraderRegistry, 52 | partitions: System.schedulers_online() 53 | ]}, 54 | id: :trader_registry 55 | ), 56 | APXR.ReportingService, 57 | {APXR.Exchange, [:apxr, :apxr, @init_price, @init_vol]}, 58 | {APXR.TraderSupervisor, trader_config()}, 59 | {APXR.Market, trader_config()} 60 | ] 61 | 62 | # See https://hexdocs.pm/elixir/Supervisor.html 63 | # for other strategies and supported options 64 | opts = [strategy: :one_for_all, name: APXR.RunSupervisor] 65 | Supervisor.init(children, opts) 66 | end 67 | 68 | # Private 69 | 70 | defp trader_config do 71 | [ 72 | %{ 73 | lcs: @liquidity_consumers, 74 | mms: @market_makers, 75 | mrts: @mean_reversion_traders, 76 | mmts: @momentum_traders, 77 | nts: @noise_traders, 78 | myts: @my_traders 79 | } 80 | ] 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/apxr/simulation.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.Simulation do 2 | @moduledoc """ 3 | Simulation manager. 4 | """ 5 | 6 | use GenServer 7 | 8 | alias APXR.{ 9 | Market, 10 | RunSupervisor 11 | } 12 | 13 | @total_runs 1 14 | 15 | ## Client API 16 | 17 | @doc """ 18 | Starts the Simulation server. 19 | """ 20 | def start_link(opts) when is_list(opts) do 21 | GenServer.start_link(__MODULE__, opts, name: __MODULE__) 22 | end 23 | 24 | @doc """ 25 | Starts the simulation. 26 | """ 27 | def start do 28 | GenServer.cast(__MODULE__, {:start}) 29 | end 30 | 31 | @doc """ 32 | Sent from the Market process at the end of the day. 33 | Starts a new day of stops the simulation depending on the number of runs 34 | completed. 35 | """ 36 | def run_over do 37 | GenServer.cast(__MODULE__, {:run_over}) 38 | end 39 | 40 | ## Server callbacks 41 | 42 | @impl true 43 | def init([]) do 44 | :ets.new(:run_number, [:public, :named_table, read_concurrency: true]) 45 | dir = File.cwd!() |> Path.join("/output") 46 | File.rm_rf!(dir) 47 | File.mkdir!(dir) 48 | {:ok, %{}} 49 | end 50 | 51 | @impl true 52 | def handle_cast({:start}, state) do 53 | do_run() 54 | {:noreply, state} 55 | end 56 | 57 | @impl true 58 | @spec handle_cast(any(), any()) :: no_return() 59 | def handle_cast({:run_over}, state) do 60 | do_run() 61 | {:noreply, state} 62 | end 63 | 64 | @impl true 65 | def handle_info(_msg, state) do 66 | {:noreply, state} 67 | end 68 | 69 | ## Private 70 | 71 | defp do_run() do 72 | run_number = :ets.update_counter(:run_number, :number, 1, {0, 0}) 73 | 74 | case run_number do 75 | 1 -> 76 | IO.puts("SIMULATION STARTED") 77 | Market.open(run_number) 78 | 79 | run_number when run_number > @total_runs -> 80 | IO.puts("\nRUN #{run_number - 1} ENDED") 81 | IO.puts("SIMULATION FINISHED") 82 | System.stop(0) 83 | 84 | _ -> 85 | Process.whereis(RunSupervisor) |> Process.exit(:kill) 86 | Process.sleep(30000) 87 | IO.puts("\nRUN #{run_number - 1} ENDED") 88 | Market.open(run_number) 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/apxr/trader.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.Trader do 2 | @moduledoc """ 3 | Represents a trader in the system. 4 | """ 5 | 6 | alias APXR.{ 7 | Order 8 | } 9 | 10 | @enforce_keys [ 11 | :trader_id, 12 | :type 13 | ] 14 | 15 | defstruct trader_id: nil, 16 | type: nil, 17 | cash: nil, 18 | outstanding_orders: nil 19 | 20 | @type t() :: %__MODULE__{ 21 | trader_id: tuple(), 22 | type: atom(), 23 | cash: non_neg_integer() | nil, 24 | outstanding_orders: list() | nil 25 | } 26 | 27 | @callback actuate(id :: integer()) :: :ok 28 | @callback execution_report(id :: integer(), order :: %Order{}, msg :: atom()) :: :ok 29 | end 30 | -------------------------------------------------------------------------------- /lib/apxr/trader_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule APXR.TraderSupervisor do 2 | # See https://hexdocs.pm/elixir/Supervisor.html 3 | # for other strategies and supported options 4 | 5 | @moduledoc false 6 | 7 | use Supervisor 8 | 9 | def start_link(opts) do 10 | Supervisor.start_link(__MODULE__, opts, name: __MODULE__) 11 | end 12 | 13 | @impl true 14 | def init([%{lcs: lcs, mms: mms, mrts: mrts, mmts: mmts, nts: nts, myts: myts}]) do 15 | children = 16 | liquidity_consumers(lcs) ++ 17 | market_makers(mms) ++ 18 | mean_reversion_traders(mrts) ++ 19 | momentum_traders(mmts) ++ 20 | noise_traders(nts) ++ 21 | my_traders(myts) 22 | 23 | Supervisor.init(children, strategy: :one_for_one) 24 | end 25 | 26 | # Private 27 | 28 | defp liquidity_consumers(liquidity_consumers) do 29 | for id <- 1..liquidity_consumers, 30 | do: Supervisor.child_spec({APXR.LiquidityConsumer, id}, id: {:liquidity_consumer, id}) 31 | end 32 | 33 | defp market_makers(market_makers) do 34 | for id <- 1..market_makers, 35 | do: Supervisor.child_spec({APXR.MarketMaker, id}, id: {:market_maker, id}) 36 | end 37 | 38 | defp mean_reversion_traders(mean_reversion_traders) do 39 | for id <- 1..mean_reversion_traders, 40 | do: 41 | Supervisor.child_spec({APXR.MeanReversionTrader, id}, id: {:mean_reversion_trader, id}) 42 | end 43 | 44 | defp momentum_traders(momentum_traders) do 45 | for id <- 1..momentum_traders, 46 | do: Supervisor.child_spec({APXR.MomentumTrader, id}, id: {:momentum_trader, id}) 47 | end 48 | 49 | defp noise_traders(noise_traders) do 50 | for id <- 1..noise_traders, 51 | do: Supervisor.child_spec({APXR.NoiseTrader, id}, id: {:noise_trader, id}) 52 | end 53 | 54 | defp my_traders(my_traders) do 55 | for id <- 1..my_traders, do: Supervisor.child_spec({APXR.MyTrader, id}, id: {:my_trader, id}) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule APXR.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.1.0" 5 | 6 | def project do 7 | [ 8 | app: :apxr, 9 | version: @version, 10 | elixir: "~> 1.9", 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | xref: xref(), 14 | deps: deps(), 15 | aliases: aliases(), 16 | test_coverage: [summary: true], 17 | preferred_cli_env: [check: :test] 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application. 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [ 25 | mod: {APXR.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | defp xref() do 31 | [exclude: [ApxrSh.Registry]] 32 | end 33 | 34 | # Specifies your project dependencies. 35 | # Run "mix help deps" to learn about dependencies. 36 | defp deps do 37 | [ 38 | {:benchee, "~> 1.0", only: :dev}, 39 | {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false} 40 | ] 41 | end 42 | 43 | # Aliases are shortcuts or tasks specific to the current project. 44 | defp aliases() do 45 | [ 46 | check: [ 47 | "deps.get", 48 | "deps.compile", 49 | "compile --warnings-as-errors", 50 | "format", 51 | "xref unreachable", 52 | "xref deprecated", 53 | "test" 54 | ] 55 | ] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, 4 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm"}, 6 | "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, 7 | "ex_shards": {:hex, :ex_shards, "0.2.1", "65c14d5ae71d6c4b617531f29923f8e57007e5b9874c100a11163c55ec0c7b21", [:mix], [{:ex2ms, "~> 1.4", [hex: :ex2ms, repo: "hexpm", optional: false]}, {:shards, "~> 0.5", [hex: :shards, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "shards": {:hex, :shards, "0.6.0", "678d292ad74a4598a872930f9b12251f43e97f6050287f1fb712fbfd3d282f75", [:make, :rebar3], [], "hexpm"}, 9 | } 10 | -------------------------------------------------------------------------------- /test/benchmark.exs: -------------------------------------------------------------------------------- 1 | :ets.update_counter(:run_number, :number, 1, {0, 0}) 2 | :ets.update_counter(:timestep, :step, 1, {0, 0}) 3 | 4 | Benchee.run(%{ 5 | "insert and cancel a buy order": fn -> 6 | order1 = APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 7 | APXR.Exchange.cancel_order(:apxr, :apxr, order1) 8 | end, 9 | "insert and cancel a sell order": fn -> 10 | order2 = APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 11 | APXR.Exchange.cancel_order(:apxr, :apxr, order2) 12 | end 13 | }) 14 | 15 | Benchee.run(%{ 16 | "insert a buy market order": fn -> 17 | APXR.Exchange.buy_market_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100) 18 | end 19 | }) 20 | 21 | Benchee.run(%{ 22 | "insert a sell market order": fn -> 23 | APXR.Exchange.sell_market_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100) 24 | end 25 | }) 26 | 27 | Benchee.run(%{ 28 | "insert a buy limit order": fn -> 29 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 30 | end, 31 | "insert a sell limit order": fn -> 32 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 33 | end, 34 | "insert a buy then sell limit order": fn -> 35 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 36 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 37 | end, 38 | "insert 10 sell limit order": fn -> 39 | for _ <- 0..10 do 40 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 41 | end 42 | end, 43 | "insert 10 buy limit order": fn -> 44 | for _ <- 0..10 do 45 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 46 | end 47 | end, 48 | "insert 10 matching orders": fn -> 49 | for _ <- 0..10 do 50 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 51 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 52 | end 53 | end, 54 | "insert 100 sell limit order": fn -> 55 | for _ <- 0..100 do 56 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 57 | end 58 | end, 59 | "insert 100 buy limit order": fn -> 60 | for _ <- 0..100 do 61 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 62 | end 63 | end, 64 | "insert 100 matching orders": fn -> 65 | for _ <- 0..100 do 66 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 67 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 68 | end 69 | end, 70 | "insert 1000 sell limit order": fn -> 71 | for _ <- 0..1000 do 72 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 73 | end 74 | end, 75 | "insert 1000 buy limit order": fn -> 76 | for _ <- 0..1000 do 77 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 78 | end 79 | end, 80 | "insert 1000 matching orders": fn -> 81 | for _ <- 0..1000 do 82 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 83 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 84 | end 85 | end 86 | }) 87 | 88 | Benchee.run(%{ 89 | "get bid_price": fn -> 90 | APXR.Exchange.bid_price(:apxr, :apxr) 91 | end, 92 | "get ask_price": fn -> 93 | APXR.Exchange.ask_price(:apxr, :apxr) 94 | end 95 | }) 96 | -------------------------------------------------------------------------------- /test/exchange_test.exs: -------------------------------------------------------------------------------- 1 | defmodule APXR.ExchangeTest do 2 | use ExUnit.Case, async: false 3 | 4 | doctest APXR.Exchange 5 | 6 | alias APXR.{ 7 | Exchange, 8 | OrderbookEvent, 9 | Order, 10 | ReportingService 11 | } 12 | 13 | setup_all do 14 | :ets.update_counter(:run_number, :number, 1, {0, 0}) 15 | :ets.update_counter(:timestep, :step, 1, {0, 0}) 16 | 17 | ReportingService.prep(1) 18 | 19 | %Order{} = Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 99.99, 100) 20 | %Order{} = Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.01, 100) 21 | 22 | %{venue: :apxr, ticker: :apxr, trader: {APXR.NoiseTrader, 1}} 23 | end 24 | 25 | test "exchange", %{venue: venue, ticker: ticker, trader: trader} do 26 | Registry.register(APXR.ReportingServiceRegistry, "orderbook_event", []) 27 | 28 | assert [99.99] = Exchange.highest_bid_prices(venue, ticker) 29 | assert [100] = Exchange.highest_bid_sizes(venue, ticker) 30 | assert [100.01] = Exchange.lowest_ask_prices(venue, ticker) 31 | assert [100] = Exchange.lowest_ask_sizes(venue, ticker) 32 | 33 | ### Buy market order 34 | 35 | # create a buy market order for 0 volume is rejected 36 | assert :rejected = Exchange.buy_market_order(venue, ticker, trader, 0) 37 | 38 | # create a buy market when volume equal to matching volume 39 | assert %Order{} = Exchange.buy_market_order(venue, ticker, trader, 100) 40 | 41 | assert_receive {:broadcast, 42 | %OrderbookEvent{ 43 | direction: 0, 44 | transaction: true, 45 | type: :full_fill_buy_order, 46 | volume: 100 47 | }} 48 | 49 | # create a buy market order with no matching opposite 50 | assert %Order{} = Exchange.buy_market_order(venue, ticker, trader, 100) 51 | 52 | refute_receive {:broadcast, 53 | %OrderbookEvent{ 54 | direction: 0, 55 | transaction: true, 56 | volume: 100 57 | }} 58 | 59 | # create a buy market when volume less than matching volume 60 | Exchange.sell_limit_order(venue, ticker, trader, 100.0, 100) 61 | 62 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 63 | assert [100] = Exchange.lowest_ask_sizes(venue, ticker) 64 | 65 | assert %Order{} = Exchange.buy_market_order(venue, ticker, trader, 10) 66 | 67 | assert_receive {:broadcast, 68 | %OrderbookEvent{ 69 | direction: 0, 70 | transaction: true, 71 | type: :full_fill_buy_order, 72 | volume: 10 73 | }} 74 | 75 | # create a buy market when volume greater than matching volume 76 | assert %Order{volume: 910} = Exchange.buy_market_order(venue, ticker, trader, 1000) 77 | 78 | assert_receive {:broadcast, 79 | %OrderbookEvent{ 80 | direction: 0, 81 | transaction: true, 82 | type: :partial_fill_buy_order, 83 | volume: 90 84 | }} 85 | 86 | ### Sell market order 87 | 88 | assert [99.99] = Exchange.highest_bid_prices(venue, ticker) 89 | assert [100] = Exchange.highest_bid_sizes(venue, ticker) 90 | 91 | # create a sell market order for 0 volume is rejected 92 | assert :rejected = Exchange.sell_market_order(venue, ticker, trader, 0) 93 | 94 | # create a sell market when volume equal to matching volume 95 | assert %Order{} = Exchange.sell_market_order(venue, ticker, trader, 100) 96 | 97 | assert_receive {:broadcast, 98 | %OrderbookEvent{ 99 | direction: 1, 100 | transaction: true, 101 | type: :full_fill_sell_order, 102 | volume: 100 103 | }} 104 | 105 | # create a sell market order with no matching opposite 106 | assert %Order{} = Exchange.sell_market_order(venue, ticker, trader, 100) 107 | 108 | refute_receive {:broadcast, 109 | %OrderbookEvent{ 110 | direction: 1, 111 | transaction: true, 112 | volume: 100 113 | }} 114 | 115 | # create a sell market when volume less than matching volume 116 | Exchange.buy_limit_order(venue, ticker, trader, 100.0, 100) 117 | 118 | assert %Order{} = Exchange.sell_market_order(venue, ticker, trader, 10) 119 | 120 | assert_receive {:broadcast, 121 | %OrderbookEvent{ 122 | direction: 1, 123 | transaction: true, 124 | type: :full_fill_sell_order, 125 | volume: 10 126 | }} 127 | 128 | # create a sell market when volume greater than matching volume 129 | assert %Order{volume: 910} = Exchange.sell_market_order(venue, ticker, trader, 1000) 130 | 131 | assert_receive {:broadcast, 132 | %OrderbookEvent{ 133 | direction: 1, 134 | transaction: true, 135 | type: :partial_fill_sell_order, 136 | volume: 90 137 | }} 138 | 139 | ### Buy limit order 140 | 141 | # create a buy limit order for 0 volume is rejected 142 | assert :rejected = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 0) 143 | 144 | # create a buy limit order for 0 price is rejected 145 | assert :rejected = Exchange.buy_limit_order(venue, ticker, trader, 0.0, 100) 146 | 147 | # create a buy limit when volume equal to matching volume 148 | Exchange.sell_limit_order(venue, ticker, trader, 100.0, 100) 149 | 150 | assert %Order{} = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 100) 151 | 152 | assert_receive {:broadcast, 153 | %OrderbookEvent{ 154 | direction: 0, 155 | transaction: true, 156 | type: :full_fill_buy_order, 157 | volume: 100 158 | }} 159 | 160 | # create a buy limit order with no matching opposite 161 | assert %Order{} = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 100) 162 | 163 | refute_receive {:broadcast, 164 | %OrderbookEvent{ 165 | direction: 0, 166 | transaction: true, 167 | volume: 100 168 | }} 169 | 170 | # create a buy limit when volume less than matching volume 171 | Exchange.sell_limit_order(venue, ticker, trader, 100.0, 200) 172 | 173 | assert [] = Exchange.highest_bid_prices(venue, ticker) 174 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 175 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 176 | assert [100] = Exchange.lowest_ask_sizes(venue, ticker) 177 | 178 | assert %Order{} = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 10) 179 | 180 | assert_receive {:broadcast, 181 | %OrderbookEvent{ 182 | direction: 0, 183 | transaction: true, 184 | type: :full_fill_buy_order, 185 | volume: 10 186 | }} 187 | 188 | # create a buy limit when volume greater than matching volume 189 | assert %Order{volume: 910} = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 1000) 190 | 191 | assert_receive {:broadcast, 192 | %OrderbookEvent{ 193 | direction: 0, 194 | transaction: true, 195 | type: :partial_fill_buy_order, 196 | volume: 90 197 | }} 198 | 199 | ### Sell limit order 200 | Exchange.sell_limit_order(venue, ticker, trader, 100.0, 910) 201 | 202 | assert [] = Exchange.highest_bid_prices(venue, ticker) 203 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 204 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 205 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 206 | 207 | # create a sell limit order for 0 volume is rejected 208 | assert :rejected = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 0) 209 | 210 | # create a sell limit order for 0 price is rejected 211 | assert :rejected = Exchange.sell_limit_order(venue, ticker, trader, 0.0, 100) 212 | 213 | # create a sell limit when volume equal to matching volume 214 | Exchange.buy_limit_order(venue, ticker, trader, 100.0, 100) 215 | 216 | assert %Order{} = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 100) 217 | 218 | assert_receive {:broadcast, 219 | %OrderbookEvent{ 220 | direction: 1, 221 | transaction: true, 222 | type: :full_fill_sell_order, 223 | volume: 100 224 | }} 225 | 226 | # create a sell limit order with no matching opposite 227 | assert %Order{} = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 75) 228 | 229 | refute_receive {:broadcast, 230 | %OrderbookEvent{ 231 | direction: 1, 232 | transaction: true, 233 | volume: 75 234 | }} 235 | 236 | # create a sell limit when volume less than matching volume 237 | Exchange.buy_limit_order(venue, ticker, trader, 100.0, 175) 238 | 239 | assert [100.0] = Exchange.highest_bid_prices(venue, ticker) 240 | assert [100] = Exchange.highest_bid_sizes(venue, ticker) 241 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 242 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 243 | 244 | assert %Order{} = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 10) 245 | 246 | assert_receive {:broadcast, 247 | %OrderbookEvent{ 248 | direction: 1, 249 | transaction: true, 250 | type: :full_fill_sell_order, 251 | volume: 10 252 | }} 253 | 254 | # create a sell limit when volume greater than matching volume 255 | assert %Order{volume: 910} = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 1000) 256 | 257 | assert_receive {:broadcast, 258 | %OrderbookEvent{ 259 | direction: 1, 260 | transaction: true, 261 | type: :partial_fill_sell_order, 262 | volume: 90 263 | }} 264 | 265 | ### Cancel order 266 | 267 | Exchange.buy_limit_order(venue, ticker, trader, 100.0, 910) 268 | 269 | assert [] = Exchange.highest_bid_prices(venue, ticker) 270 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 271 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 272 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 273 | 274 | # cancel existing buy order 275 | assert %Order{} = order = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 50) 276 | 277 | assert [100.0] = Exchange.highest_bid_prices(venue, ticker) 278 | assert [50] = Exchange.highest_bid_sizes(venue, ticker) 279 | 280 | assert :ok = Exchange.cancel_order(venue, ticker, order) 281 | 282 | assert [] = Exchange.highest_bid_prices(venue, ticker) 283 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 284 | 285 | assert_receive {:broadcast, 286 | %OrderbookEvent{ 287 | direction: 0, 288 | type: :cancel_limit_order, 289 | volume: 50 290 | }} 291 | 292 | # cancel existing sell order 293 | assert %Order{} = order = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 50) 294 | 295 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 296 | assert [50] = Exchange.lowest_ask_sizes(venue, ticker) 297 | 298 | assert :ok = Exchange.cancel_order(venue, ticker, order) 299 | 300 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 301 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 302 | 303 | assert_receive {:broadcast, 304 | %OrderbookEvent{ 305 | direction: 1, 306 | type: :cancel_limit_order, 307 | volume: 50 308 | }} 309 | 310 | assert [] = Exchange.highest_bid_prices(venue, ticker) 311 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 312 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 313 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 314 | 315 | # cancel partially matched buy order 316 | assert %Order{} = order = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 50) 317 | 318 | assert %Order{} = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 25) 319 | 320 | assert [100.0] = Exchange.highest_bid_prices(venue, ticker) 321 | assert [25] = Exchange.highest_bid_sizes(venue, ticker) 322 | 323 | assert :ok = Exchange.cancel_order(venue, ticker, order) 324 | 325 | assert [] = Exchange.highest_bid_prices(venue, ticker) 326 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 327 | 328 | assert_receive {:broadcast, 329 | %OrderbookEvent{ 330 | direction: 0, 331 | type: :cancel_limit_order, 332 | volume: 50 333 | }} 334 | 335 | # cancel partially matched sell order 336 | assert %Order{} = order = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 50) 337 | 338 | assert %Order{} = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 25) 339 | 340 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 341 | assert [25] = Exchange.lowest_ask_sizes(venue, ticker) 342 | 343 | assert :ok = Exchange.cancel_order(venue, ticker, order) 344 | 345 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 346 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 347 | 348 | assert_receive {:broadcast, 349 | %OrderbookEvent{ 350 | direction: 1, 351 | type: :cancel_limit_order, 352 | volume: 50 353 | }} 354 | 355 | assert [] = Exchange.highest_bid_prices(venue, ticker) 356 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 357 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 358 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 359 | 360 | # cancel buy order that does not exist 361 | assert %Order{} = order = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 50) 362 | 363 | assert %Order{} = other_order = Exchange.buy_limit_order(venue, ticker, trader, 100.0, 50) 364 | 365 | assert [100.0] = Exchange.highest_bid_prices(venue, ticker) 366 | assert [100] = Exchange.highest_bid_sizes(venue, ticker) 367 | 368 | assert :ok = Exchange.cancel_order(venue, ticker, order) 369 | 370 | assert [100.0] = Exchange.highest_bid_prices(venue, ticker) 371 | assert [50] = Exchange.highest_bid_sizes(venue, ticker) 372 | 373 | assert :ok = Exchange.cancel_order(venue, ticker, order) 374 | 375 | assert [100.0] = Exchange.highest_bid_prices(venue, ticker) 376 | assert [50] = Exchange.highest_bid_sizes(venue, ticker) 377 | 378 | assert :ok = Exchange.cancel_order(venue, ticker, other_order) 379 | 380 | assert [] = Exchange.highest_bid_prices(venue, ticker) 381 | assert [] = Exchange.highest_bid_sizes(venue, ticker) 382 | 383 | # cancel sell order that does not exist 384 | assert %Order{} = order = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 50) 385 | 386 | assert %Order{} = other_order = Exchange.sell_limit_order(venue, ticker, trader, 100.0, 50) 387 | 388 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 389 | assert [100] = Exchange.lowest_ask_sizes(venue, ticker) 390 | 391 | assert :ok = Exchange.cancel_order(venue, ticker, order) 392 | 393 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 394 | assert [50] = Exchange.lowest_ask_sizes(venue, ticker) 395 | 396 | assert :ok = Exchange.cancel_order(venue, ticker, order) 397 | 398 | assert [100.0] = Exchange.lowest_ask_prices(venue, ticker) 399 | assert [50] = Exchange.lowest_ask_sizes(venue, ticker) 400 | 401 | assert :ok = Exchange.cancel_order(venue, ticker, other_order) 402 | 403 | assert [] = Exchange.lowest_ask_prices(venue, ticker) 404 | assert [] = Exchange.lowest_ask_sizes(venue, ticker) 405 | 406 | ### 407 | Exchange.buy_market_order(venue, ticker, trader, 500) 408 | Exchange.sell_market_order(venue, ticker, trader, 500) 409 | 410 | Exchange.buy_limit_order(venue, ticker, trader, 99.99, 100) 411 | Exchange.sell_limit_order(venue, ticker, trader, 100.01, 100) 412 | 413 | assert [99.99] = Exchange.highest_bid_prices(venue, ticker) 414 | assert [100] = Exchange.highest_bid_sizes(venue, ticker) 415 | assert [100.01] = Exchange.lowest_ask_prices(venue, ticker) 416 | assert [100] = Exchange.lowest_ask_sizes(venue, ticker) 417 | ### 418 | 419 | # Bid price & bid size 420 | assert 99.99 = APXR.Exchange.bid_price(:apxr, :apxr) 421 | assert 100 = APXR.Exchange.bid_size(:apxr, :apxr) 422 | 423 | Exchange.sell_market_order(venue, ticker, trader, 100) 424 | 425 | assert 0.0 = APXR.Exchange.bid_price(:apxr, :apxr) 426 | assert 0 = APXR.Exchange.bid_size(:apxr, :apxr) 427 | 428 | Exchange.buy_limit_order(venue, ticker, trader, 100.0, 100) 429 | 430 | assert 100.0 = APXR.Exchange.bid_price(:apxr, :apxr) 431 | assert 100 = APXR.Exchange.bid_size(:apxr, :apxr) 432 | 433 | Exchange.buy_limit_order(venue, ticker, trader, 50.0, 100) 434 | Exchange.buy_limit_order(venue, ticker, trader, 125.0, 100) 435 | Exchange.buy_limit_order(venue, ticker, trader, 150.0, 100) 436 | Exchange.buy_limit_order(venue, ticker, trader, 100.0, 100) 437 | 438 | assert 150.0 = APXR.Exchange.bid_price(:apxr, :apxr) 439 | assert 100 = APXR.Exchange.bid_size(:apxr, :apxr) 440 | 441 | Exchange.buy_limit_order(venue, ticker, trader, 250.0, 100) 442 | Exchange.buy_limit_order(venue, ticker, trader, 50.0, 100) 443 | 444 | assert 250.0 = APXR.Exchange.bid_price(:apxr, :apxr) 445 | assert 100 = APXR.Exchange.bid_size(:apxr, :apxr) 446 | 447 | assert [250.0, 150.0, 100.0, 50.0] = Exchange.highest_bid_prices(venue, ticker) 448 | assert [100, 100, 200, 200] = Exchange.highest_bid_sizes(venue, ticker) 449 | 450 | Exchange.sell_market_order(venue, ticker, trader, 1000) 451 | 452 | # Last price & last size 1/2 453 | assert 50.0 = APXR.Exchange.last_price(:apxr, :apxr) 454 | assert 100 = APXR.Exchange.last_size(:apxr, :apxr) 455 | 456 | # Ask price & ask size 457 | assert 0.0 = APXR.Exchange.ask_price(:apxr, :apxr) 458 | assert 0 = APXR.Exchange.ask_size(:apxr, :apxr) 459 | 460 | Exchange.sell_limit_order(venue, ticker, trader, 100.0, 100) 461 | 462 | assert 100.0 = APXR.Exchange.ask_price(:apxr, :apxr) 463 | assert 100 = APXR.Exchange.ask_size(:apxr, :apxr) 464 | 465 | Exchange.sell_limit_order(venue, ticker, trader, 150.0, 100) 466 | Exchange.sell_limit_order(venue, ticker, trader, 50.0, 100) 467 | Exchange.sell_limit_order(venue, ticker, trader, 100.0, 100) 468 | 469 | assert 50.0 = APXR.Exchange.ask_price(:apxr, :apxr) 470 | assert 100 = APXR.Exchange.ask_size(:apxr, :apxr) 471 | 472 | Exchange.sell_limit_order(venue, ticker, trader, 50.0, 100) 473 | Exchange.sell_limit_order(venue, ticker, trader, 250.0, 100) 474 | 475 | assert 50.0 = APXR.Exchange.ask_price(:apxr, :apxr) 476 | assert 200 = APXR.Exchange.ask_size(:apxr, :apxr) 477 | 478 | assert [50.0, 100.0, 150.0, 250.0] = Exchange.lowest_ask_prices(venue, ticker) 479 | assert [200, 200, 100, 100] = Exchange.lowest_ask_sizes(venue, ticker) 480 | 481 | Exchange.buy_market_order(venue, ticker, trader, 1000) 482 | 483 | # Last price & last size 2/2 484 | assert 250.0 = APXR.Exchange.last_price(:apxr, :apxr) 485 | assert 100 = APXR.Exchange.last_size(:apxr, :apxr) 486 | 487 | assert [] = APXR.Exchange.highest_bid_prices(:apxr, :apxr) 488 | assert [] = APXR.Exchange.highest_bid_sizes(:apxr, :apxr) 489 | 490 | assert [] = APXR.Exchange.lowest_ask_prices(:apxr, :apxr) 491 | assert [] = APXR.Exchange.lowest_ask_sizes(:apxr, :apxr) 492 | 493 | # Highest bid prices & highest bid sizes 494 | Exchange.buy_limit_order(venue, ticker, trader, 350.0, 100) 495 | Exchange.buy_limit_order(venue, ticker, trader, 450.0, 100) 496 | Exchange.buy_limit_order(venue, ticker, trader, 250.0, 100) 497 | 498 | assert [450.0, 350.0, 250.0] = APXR.Exchange.highest_bid_prices(:apxr, :apxr) 499 | assert [100, 100, 100] = APXR.Exchange.highest_bid_sizes(:apxr, :apxr) 500 | 501 | # Lowest ask prices & lowest ask sizes 502 | Exchange.sell_limit_order(venue, ticker, trader, 50.0, 100) 503 | Exchange.sell_limit_order(venue, ticker, trader, 75.0, 100) 504 | Exchange.sell_limit_order(venue, ticker, trader, 25.0, 100) 505 | 506 | assert [] = APXR.Exchange.highest_bid_prices(:apxr, :apxr) 507 | assert [] = APXR.Exchange.highest_bid_sizes(:apxr, :apxr) 508 | 509 | Exchange.sell_limit_order(venue, ticker, trader, 50.0, 100) 510 | Exchange.sell_limit_order(venue, ticker, trader, 75.0, 100) 511 | Exchange.sell_limit_order(venue, ticker, trader, 25.0, 100) 512 | 513 | assert [25.0, 50.0, 75.0] = APXR.Exchange.lowest_ask_prices(:apxr, :apxr) 514 | assert [100, 100, 100] = APXR.Exchange.lowest_ask_sizes(:apxr, :apxr) 515 | 516 | ### 517 | Exchange.buy_market_order(venue, ticker, trader, 300) 518 | Exchange.sell_market_order(venue, ticker, trader, 300) 519 | 520 | Exchange.buy_limit_order(venue, ticker, trader, 99.99, 100) 521 | Exchange.sell_limit_order(venue, ticker, trader, 100.01, 100) 522 | 523 | assert [99.99] = Exchange.highest_bid_prices(venue, ticker) 524 | assert [100] = Exchange.highest_bid_sizes(venue, ticker) 525 | assert [100.01] = Exchange.lowest_ask_prices(venue, ticker) 526 | assert [100] = Exchange.lowest_ask_sizes(venue, ticker) 527 | end 528 | end 529 | -------------------------------------------------------------------------------- /test/nimble_csv_test.exs: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Plataformatec 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | defmodule NimbleCSVTest do 16 | use ExUnit.Case 17 | 18 | alias NimbleCSV.RFC4180, as: CSV 19 | 20 | test "parse_string/2" do 21 | assert CSV.parse_string(""" 22 | name,last,year 23 | john,doe,1986 24 | """) == [~w(john doe 1986)] 25 | end 26 | 27 | test "parse_string/2 without headers" do 28 | assert CSV.parse_string( 29 | """ 30 | name,last,year 31 | john,doe,1986 32 | """, 33 | skip_headers: false 34 | ) == [~w(name last year), ~w(john doe 1986)] 35 | 36 | assert CSV.parse_string( 37 | """ 38 | name,last,year 39 | john,doe,1986 40 | mary,jane,1985 41 | """, 42 | skip_headers: false 43 | ) == [~w(name last year), ~w(john doe 1986), ~w(mary jane 1985)] 44 | end 45 | 46 | test "parse_string/2 without trailing new line" do 47 | assert CSV.parse_string( 48 | string_trim(""" 49 | name,last,year 50 | john,doe,1986 51 | mary,jane,1985 52 | """) 53 | ) == [~w(john doe 1986), ~w(mary jane 1985)] 54 | end 55 | 56 | test "parse_string/2 with CRLF terminations" do 57 | assert CSV.parse_string("name,last,year\r\njohn,doe,1986\r\n") == [~w(john doe 1986)] 58 | end 59 | 60 | test "parse_string/2 with empty string" do 61 | assert CSV.parse_string("", skip_headers: false) == [] 62 | 63 | assert CSV.parse_string( 64 | """ 65 | name 66 | 67 | john 68 | 69 | """, 70 | skip_headers: false 71 | ) == [["name"], [""], ["john"], [""]] 72 | end 73 | 74 | test "parse_string/2 with whitespace" do 75 | assert CSV.parse_string(""" 76 | name,last,year 77 | \sjohn , doe , 1986\s 78 | """) == [[" john ", " doe ", " 1986 "]] 79 | end 80 | 81 | test "parse_string/2 with escape characters" do 82 | assert CSV.parse_string(""" 83 | name,last,year 84 | john,"doe",1986 85 | """) == [~w(john doe 1986)] 86 | 87 | assert CSV.parse_string(""" 88 | name,last,year 89 | "john",doe,"1986" 90 | """) == [~w(john doe 1986)] 91 | 92 | assert CSV.parse_string(""" 93 | name,last,year 94 | "john","doe","1986" 95 | mary,"jane",1985 96 | """) == [~w(john doe 1986), ~w(mary jane 1985)] 97 | 98 | assert CSV.parse_string(""" 99 | name,year 100 | "doe, john",1986 101 | "jane, mary",1985 102 | """) == [["doe, john", "1986"], ["jane, mary", "1985"]] 103 | end 104 | 105 | test "parse_string/2 with escape characters spawning multiple lines" do 106 | assert CSV.parse_string(""" 107 | name,last,comments 108 | john,"doe","this is a 109 | really long comment 110 | with multiple lines" 111 | mary,jane,short comment 112 | """) == [ 113 | ["john", "doe", "this is a\nreally long comment\nwith multiple lines"], 114 | ["mary", "jane", "short comment"] 115 | ] 116 | end 117 | 118 | test "parse_string/2 with escaped escape characters" do 119 | assert CSV.parse_string(""" 120 | name,last,comments 121 | john,"doe","with ""double-quotes"" inside" 122 | mary,jane,"with , inside" 123 | """) == [ 124 | ["john", "doe", "with \"double-quotes\" inside"], 125 | ["mary", "jane", "with , inside"] 126 | ] 127 | end 128 | 129 | test "parse_string/2 with invalid escape" do 130 | assert_raise NimbleCSV.ParseError, 131 | ~s(unexpected escape character " in "john,d\\\"e,1986\\n"), 132 | fn -> 133 | CSV.parse_string(""" 134 | name,last,year 135 | john,d"e,1986 136 | """) 137 | end 138 | 139 | assert_raise NimbleCSV.ParseError, 140 | ~s(unexpected escape character " in "d\\\"e,1986\\n"), 141 | fn -> 142 | CSV.parse_string(""" 143 | name,last,year 144 | john,"d"e,1986 145 | """) 146 | end 147 | 148 | assert_raise NimbleCSV.ParseError, 149 | ~s(expected escape character " but reached the end of file), 150 | fn -> 151 | CSV.parse_string(""" 152 | name,last,year 153 | john,doe,"1986 154 | """) 155 | end 156 | end 157 | 158 | test "parse_enumerable/2" do 159 | assert CSV.parse_enumerable([ 160 | "name,last,year\n", 161 | "john,doe,1986\n" 162 | ]) == [~w(john doe 1986)] 163 | 164 | assert CSV.parse_enumerable( 165 | [ 166 | "name,last,year\n", 167 | "john,doe,1986\n" 168 | ], 169 | skip_headers: false 170 | ) == [~w(name last year), ~w(john doe 1986)] 171 | 172 | assert_raise NimbleCSV.ParseError, 173 | ~s(expected escape character " but reached the end of file), 174 | fn -> 175 | CSV.parse_enumerable([ 176 | "name,last,year\n", 177 | "john,doe,\"1986\n" 178 | ]) 179 | end 180 | end 181 | 182 | test "parse_stream/2" do 183 | stream = 184 | [ 185 | "name,last,year\n", 186 | "john,doe,1986\n" 187 | ] 188 | |> Stream.map(&String.upcase/1) 189 | 190 | assert CSV.parse_stream(stream) |> Enum.to_list() == [~w(JOHN DOE 1986)] 191 | 192 | stream = 193 | [ 194 | "name,last,year\n", 195 | "john,doe,1986\n" 196 | ] 197 | |> Stream.map(&String.upcase/1) 198 | 199 | assert CSV.parse_stream(stream, skip_headers: false) |> Enum.to_list() == 200 | [~w(NAME LAST YEAR), ~w(JOHN DOE 1986)] 201 | 202 | stream = 203 | CSV.parse_stream( 204 | [ 205 | "name,last,year\n", 206 | "john,doe,\"1986\n" 207 | ] 208 | |> Stream.map(&String.upcase/1) 209 | ) 210 | 211 | assert_raise NimbleCSV.ParseError, 212 | ~s(expected escape character " but reached the end of file), 213 | fn -> 214 | Enum.to_list(stream) 215 | end 216 | end 217 | 218 | test "dump_to_iodata/1" do 219 | assert IO.iodata_to_binary(CSV.dump_to_iodata([["name", "age"], ["john", 27]])) == """ 220 | name,age 221 | john,27 222 | """ 223 | 224 | assert IO.iodata_to_binary(CSV.dump_to_iodata([["name", "age"], ["john\ndoe", 27]])) == """ 225 | name,age 226 | "john 227 | doe",27 228 | """ 229 | 230 | assert IO.iodata_to_binary(CSV.dump_to_iodata([["name", "age"], ["john \"nick\" doe", 27]])) == 231 | """ 232 | name,age 233 | "john ""nick"" doe",27 234 | """ 235 | 236 | assert IO.iodata_to_binary(CSV.dump_to_iodata([["name", "age"], ["doe, john", 27]])) == """ 237 | name,age 238 | "doe, john",27 239 | """ 240 | end 241 | 242 | test "dump_to_stream/1" do 243 | assert IO.iodata_to_binary(Enum.to_list(CSV.dump_to_stream([["name", "age"], ["john", 27]]))) == 244 | """ 245 | name,age 246 | john,27 247 | """ 248 | 249 | assert IO.iodata_to_binary( 250 | Enum.to_list(CSV.dump_to_stream([["name", "age"], ["john\ndoe", 27]])) 251 | ) == """ 252 | name,age 253 | "john 254 | doe",27 255 | """ 256 | 257 | assert IO.iodata_to_binary( 258 | Enum.to_list(CSV.dump_to_stream([["name", "age"], ["john \"nick\" doe", 27]])) 259 | ) == """ 260 | name,age 261 | "john ""nick"" doe",27 262 | """ 263 | end 264 | 265 | describe "multiple separators" do 266 | NimbleCSV.define(CSVWithUnknownSeparator, separator: [",", ";", "\t"]) 267 | 268 | test "parse_string/2 (unknown separator)" do 269 | assert CSVWithUnknownSeparator.parse_string(""" 270 | name,last\tyear 271 | john;doe,1986 272 | """) == [~w(john doe 1986)] 273 | end 274 | 275 | test "parse_stream/2 (unknown separator)" do 276 | stream = 277 | [ 278 | "name,last\tyear\n", 279 | "john;doe,1986\n" 280 | ] 281 | |> Stream.map(&String.upcase/1) 282 | 283 | assert CSVWithUnknownSeparator.parse_stream(stream) |> Enum.to_list() == [~w(JOHN DOE 1986)] 284 | 285 | stream = 286 | [ 287 | "name,last\tyear\n", 288 | "john;doe,1986\n" 289 | ] 290 | |> Stream.map(&String.upcase/1) 291 | 292 | assert CSVWithUnknownSeparator.parse_stream(stream, skip_headers: false) |> Enum.to_list() == 293 | [~w(NAME LAST YEAR), ~w(JOHN DOE 1986)] 294 | 295 | stream = 296 | CSVWithUnknownSeparator.parse_stream( 297 | [ 298 | "name,last\tyear\n", 299 | "john;doe,\"1986\n" 300 | ] 301 | |> Stream.map(&String.upcase/1) 302 | ) 303 | 304 | assert_raise NimbleCSV.ParseError, 305 | ~s(expected escape character " but reached the end of file), 306 | fn -> 307 | Enum.to_list(stream) 308 | end 309 | end 310 | 311 | test "dump_to_iodata/1 (unknown separator)" do 312 | assert IO.iodata_to_binary( 313 | CSVWithUnknownSeparator.dump_to_iodata([["name", "age"], ["john", 27]]) 314 | ) == """ 315 | name,age 316 | john,27 317 | """ 318 | 319 | assert IO.iodata_to_binary( 320 | CSVWithUnknownSeparator.dump_to_iodata([["name", "age"], ["john\ndoe", 27]]) 321 | ) == """ 322 | name,age 323 | "john 324 | doe",27 325 | """ 326 | 327 | assert IO.iodata_to_binary( 328 | CSVWithUnknownSeparator.dump_to_iodata([["name", "age"], ["john \"nick\" doe", 27]]) 329 | ) == """ 330 | name,age 331 | "john ""nick"" doe",27 332 | """ 333 | 334 | assert IO.iodata_to_binary( 335 | CSVWithUnknownSeparator.dump_to_iodata([["name", "age"], ["doe, john", 27]]) 336 | ) == """ 337 | name,age 338 | "doe, john",27 339 | """ 340 | end 341 | 342 | test "dump_to_stream/1 (unknown separator)" do 343 | assert IO.iodata_to_binary( 344 | Enum.to_list( 345 | CSVWithUnknownSeparator.dump_to_stream([["name", "age"], ["john", 27]]) 346 | ) 347 | ) == """ 348 | name,age 349 | john,27 350 | """ 351 | 352 | assert IO.iodata_to_binary( 353 | Enum.to_list( 354 | CSVWithUnknownSeparator.dump_to_stream([["name", "age"], ["john\ndoe", 27]]) 355 | ) 356 | ) == """ 357 | name,age 358 | "john 359 | doe",27 360 | """ 361 | 362 | assert IO.iodata_to_binary( 363 | Enum.to_list( 364 | CSVWithUnknownSeparator.dump_to_stream([ 365 | ["name", "age"], 366 | ["john \"nick\" doe", 27] 367 | ]) 368 | ) 369 | ) == """ 370 | name,age 371 | "john ""nick"" doe",27 372 | """ 373 | end 374 | 375 | test "parse_string/2 with escape characters (unknown separator)" do 376 | assert CSV.parse_string(""" 377 | name,year 378 | "doe, john",1986 379 | "jane; mary",1985 380 | """) == [["doe, john", "1986"], ["jane; mary", "1985"]] 381 | end 382 | end 383 | 384 | # TODO: Remove once we depend on Elixir 1.3 and on. 385 | Code.ensure_loaded(String) 386 | 387 | if function_exported?(String, :trim, 1) do 388 | defp string_trim(str), do: String.trim(str) 389 | else 390 | defp string_trim(str), do: String.strip(str) 391 | end 392 | end 393 | -------------------------------------------------------------------------------- /test/profile.exs: -------------------------------------------------------------------------------- 1 | :ets.update_counter(:run_number, :number, 1, {0, 0}) 2 | :ets.update_counter(:timestep, :step, 1, {0, 0}) 3 | 4 | for _ <- 0..1000 do 5 | APXR.Exchange.buy_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 6 | APXR.Exchange.sell_limit_order(:apxr, :apxr, {APXR.NoiseTrader, 1}, 100.0, 100) 7 | end 8 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /validate.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import pandas as pd\n", 11 | "import matplotlib.pyplot as plt" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "run = 1" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "mid_prices = pd.read_csv(f'/Users/admin/Workspace/apxr/output/apxr_mid_prices{run}.csv', header=None)\n", 30 | "mid_prices.columns = ['price']\n", 31 | "mid_prices['returns'] = mid_prices['price'].pct_change()" 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": null, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "plt.plot(mid_prices['price'])\n", 41 | "plt.show()" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "trade_prices = pd.read_csv(f'/Users/admin/Workspace/apxr/output/apxr_trades{run}.csv', header=None)\n", 51 | "trade_prices.columns = ['price']\n", 52 | "trade_prices['returns'] = trade_prices[\"price\"].pct_change()" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "plt.plot(trade_prices['price'])\n", 62 | "plt.show()" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## Fat tailed distribution of returns" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "time = []\n", 79 | "kurt = []\n", 80 | "\n", 81 | "for lag in range(2500):\n", 82 | " lagged_returns = mid_prices['price'].pct_change(lag)\n", 83 | " kurtosis = lagged_returns.kurt()\n", 84 | " kurt.append(kurtosis)\n", 85 | " time.append(lag)\n", 86 | "\n", 87 | "# Remove first observation since it is zero and Nan\n", 88 | "time = time[1:]\n", 89 | "kurt = kurt[1:]" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "plt.figure(figsize=(10, 7))\n", 99 | "\n", 100 | "plt.plot(time, kurt, label='Kurt')\n", 101 | "plt.legend()\n", 102 | "plt.xlabel('Time scale')\n", 103 | "plt.ylabel('Kurtosis')\n", 104 | "plt.title('Kurtosis of returns and lags')\n", 105 | "plt.tight_layout()" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "## Volatility clustering" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "import sys\n", 122 | "!{sys.executable} -m pip install hurst" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": null, 128 | "metadata": {}, 129 | "outputs": [], 130 | "source": [ 131 | "from hurst import compute_Hc\n", 132 | "\n", 133 | "H_list = []\n", 134 | "\n", 135 | "for lag in range(1, 2500):\n", 136 | " H, c, data = compute_Hc(mid_prices[\"price\"].pct_change(lag).dropna().abs(), kind='change', simplified=True)\n", 137 | " H_list.append(H)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "f, ax = plt.subplots(figsize=(10, 7))\n", 147 | "plt.plot(np.arange(2499), H_list)\n", 148 | "ax.set_xlabel('Time interval')\n", 149 | "ax.set_ylabel('Hurst exponent')\n", 150 | "ax.grid(True)\n", 151 | "plt.title('Volatility clustering')\n", 152 | "plt.show()" 153 | ] 154 | }, 155 | { 156 | "cell_type": "markdown", 157 | "metadata": {}, 158 | "source": [ 159 | "## Autocorrelation of returns" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "def autocorrelation(x):\n", 169 | " result = np.correlate(x, x, mode='full')\n", 170 | " return result[result.size // 2:]" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [ 179 | "# First lag returns of mid prices\n", 180 | "returns_first_lag = mid_prices['returns']\n", 181 | "returns_first_lag_ac = autocorrelation(returns_first_lag[2:])\n", 182 | "\n", 183 | "# First lag returns of trade prices\n", 184 | "tp_first_lag = trade_prices['returns']\n", 185 | "tp_first_lag_ac = autocorrelation(tp_first_lag[2:])" 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": null, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "summary = pd.DataFrame(returns_first_lag_ac, columns=['AC mid price returns'])\n", 195 | "summary.describe()" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "summary = pd.DataFrame(tp_first_lag_ac, columns=['AC trade price returns'])\n", 205 | "summary.describe()" 206 | ] 207 | }, 208 | { 209 | "cell_type": "markdown", 210 | "metadata": {}, 211 | "source": [ 212 | "## Long memory in order flow" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "order_sides = pd.read_csv(f'/Users/admin/Workspace/apxr/output/apxr_order_sides{run}.csv', header=None)\n", 222 | "order_sides.columns = ['side']\n", 223 | "\n", 224 | "constraint = (order_sides['side'] == 0)\n", 225 | "order_sides.loc[constraint, 'side'] = order_sides['side'] - 1" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": null, 231 | "metadata": {}, 232 | "outputs": [], 233 | "source": [ 234 | "# Mean first order lag autocorrelation of the order sign series\n", 235 | "mean = np.mean(autocorrelation(order_sides['side']))\n", 236 | "print(\"AC order sign series mean:\", mean)" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "# Hurst exponent\n", 246 | "H, c, data = compute_Hc(order_sides['side'].dropna(), kind='change', simplified=True)\n", 247 | "print(\"Hurst coeff order sign series:\", H)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "## Price impact" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "price_impact = pd.read_csv(f'/Users/admin/Workspace/apxr/output/apxr_price_impacts{run}.csv', header=None)\n", 264 | "price_impact.columns = ['volume', 'impact']\n", 265 | "price_impact['volume'] = np.log(price_impact['volume'])" 266 | ] 267 | }, 268 | { 269 | "cell_type": "code", 270 | "execution_count": null, 271 | "metadata": {}, 272 | "outputs": [], 273 | "source": [ 274 | "from sklearn.linear_model import LinearRegression\n", 275 | "\n", 276 | "x = price_impact['volume'].values.reshape(-1, 1)\n", 277 | "y = price_impact['impact']\n", 278 | "\n", 279 | "model = LinearRegression()\n", 280 | "model.fit(x, y)\n", 281 | "\n", 282 | "r_sq = model.score(x, y)\n", 283 | "print('Coefficient of determination:', r_sq)\n", 284 | "print('Intercept:', model.intercept_)\n", 285 | "print('Slope:', model.coef_)" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": null, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "f, ax = plt.subplots(figsize=(10, 7))\n", 295 | "plt.plot(np.arange(1, 200000), model.coef_ * np.arange(1, 200000) + model.intercept_)\n", 296 | "ax.set_xlabel('Volume')\n", 297 | "ax.set_ylabel('Impact')\n", 298 | "ax.grid(True)\n", 299 | "plt.title('Price impact')\n", 300 | "plt.show()" 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "## Extreme price events" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": null, 313 | "metadata": {}, 314 | "outputs": [], 315 | "source": [ 316 | "def is_extreme(position, df):\n", 317 | " \"\"\"Returns true for a position if it consistently takes\n", 318 | " up/down position for 10 times before switching to the\n", 319 | " opposite and the net change is at least 0.8% of initial\n", 320 | " price\"\"\"\n", 321 | " initial_price = df.iloc[0]['price']\n", 322 | " prices = df['price'].values\n", 323 | " curr_price = prices[position]\n", 324 | " increasing = True\n", 325 | " for i in range(position + 1, position + 10):\n", 326 | " if curr_price >= prices[i]:\n", 327 | " increasing = False\n", 328 | " break\n", 329 | " if increasing:\n", 330 | " delta = abs(prices[position] - prices[position + 10])\n", 331 | " if delta > initial_price * 0.08:\n", 332 | " return True\n", 333 | " decreaing = True\n", 334 | " for i in range(position + 1, position + 10):\n", 335 | " if curr_price <= prices[i]:\n", 336 | " decreaing = False\n", 337 | " break\n", 338 | " if decreaing:\n", 339 | " delta = abs(prices[position] - prices[position + 10])\n", 340 | " if delta > initial_price * 0.08:\n", 341 | " return True\n", 342 | " return False" 343 | ] 344 | }, 345 | { 346 | "cell_type": "code", 347 | "execution_count": null, 348 | "metadata": {}, 349 | "outputs": [], 350 | "source": [ 351 | "extreme_events = []\n", 352 | "for i in range(len(mid_prices) - 10):\n", 353 | " if is_extreme(i, mid_prices):\n", 354 | " # save position\n", 355 | " extreme_events.append(i)" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": null, 361 | "metadata": {}, 362 | "outputs": [], 363 | "source": [ 364 | "print('Number of extreme price events:', len(extreme_events))" 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": null, 370 | "metadata": {}, 371 | "outputs": [], 372 | "source": [] 373 | } 374 | ], 375 | "metadata": { 376 | "kernelspec": { 377 | "display_name": "Python 3", 378 | "language": "python", 379 | "name": "python3" 380 | }, 381 | "language_info": { 382 | "codemirror_mode": { 383 | "name": "ipython", 384 | "version": 3 385 | }, 386 | "file_extension": ".py", 387 | "mimetype": "text/x-python", 388 | "name": "python", 389 | "nbconvert_exporter": "python", 390 | "pygments_lexer": "ipython3", 391 | "version": "3.7.3" 392 | } 393 | }, 394 | "nbformat": 4, 395 | "nbformat_minor": 2 396 | } 397 | --------------------------------------------------------------------------------