├── market data └── README.md ├── final model ├── readme.md └── final_simulator.py ├── README.md ├── comparison of Avellaneda-Stoikov and Gueant models ├── README.md ├── Avellaneda_Stoikov_HFT_2008.pdf ├── Gueant_2012_Dealing_with_the_Inventory_Risk_A_solution_to_the_market.pdf ├── Avellaneda_Stoikov_simulator.py └── Gueant_simulator.py ├── simulator.py └── Stoikov-implentation └── simulator.py /market data/README.md: -------------------------------------------------------------------------------- 1 | market data (BTC/ETH futures) 2 | -------------------------------------------------------------------------------- /final model/readme.md: -------------------------------------------------------------------------------- 1 | final model in HFTMM project (combination of Avellaneda-Stoikov & Gueant models) 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HFT Market Making 2 | This repo contains different market making strategies and scientific papers 3 | -------------------------------------------------------------------------------- /comparison of Avellaneda-Stoikov and Gueant models/README.md: -------------------------------------------------------------------------------- 1 | this notebook contains comparison of Avellaneda-Stoikov and Gueant HFT models. 2 | -------------------------------------------------------------------------------- /comparison of Avellaneda-Stoikov and Gueant models/Avellaneda_Stoikov_HFT_2008.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorrazzhivin/HFT-Market-Making/HEAD/comparison of Avellaneda-Stoikov and Gueant models/Avellaneda_Stoikov_HFT_2008.pdf -------------------------------------------------------------------------------- /comparison of Avellaneda-Stoikov and Gueant models/Gueant_2012_Dealing_with_the_Inventory_Risk_A_solution_to_the_market.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorrazzhivin/HFT-Market-Making/HEAD/comparison of Avellaneda-Stoikov and Gueant models/Gueant_2012_Dealing_with_the_Inventory_Risk_A_solution_to_the_market.pdf -------------------------------------------------------------------------------- /simulator.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | import pandas as pd 5 | import datetime 6 | import random 7 | 8 | @dataclass 9 | class Order: #our own placed orders 10 | order_id: int 11 | size: float 12 | price: float 13 | timestamp: datetime 14 | side: str 15 | 16 | @dataclass 17 | class AnonTrade: #trades from market data 18 | timestamp: datetime 19 | size: float 20 | price: str 21 | side: str 22 | 23 | 24 | @dataclass 25 | class OwnTrade: #successful trades 26 | timestamp: datetime 27 | trade_id: int 28 | order_id: int 29 | size: float 30 | price: float 31 | side: str 32 | 33 | 34 | @dataclass 35 | class OrderbookSnapshotUpdate: #current orderbook's shapshot 36 | timestamp: datetime 37 | asks: list[tuple[float, float]] 38 | bids: list[tuple[float, float]] 39 | 40 | 41 | @dataclass 42 | class MdUpdate: #data of a tick 43 | orderbook: Optional[OrderbookSnapshotUpdate] = None 44 | trades: Optional[list[AnonTrade]] = None 45 | 46 | 47 | class Strategy: 48 | def __init__(self, max_position: float, t_0: int, maker_fee: int, pnl_data) -> None: 49 | #we want to control maximum size of inventory that we have after each execution 50 | self.max_position = max_position 51 | #if order is waiting for placing more that t_0, than we cancel this order 52 | self.t_0 = t_0 53 | self.total_size = 0.0 #total size of our inventory 54 | self.pnl = 0 #our profits and losses 55 | self.size = 0.001 #size of each order 56 | self.maker_fee = maker_fee #fee that exchange takes 57 | self.pnl_data = pnl_data #dependence between pnl and orderbook.timestamp 58 | self.temp_bids = [] #successfully executed bids 59 | self.temp_asks = [] #successfully executed asks 60 | 61 | def run(self, sim: "Sim"): 62 | while True: 63 | try: 64 | orderbook, own_trades, orders_dict_latency = sim.tick(self.maker_fee) 65 | 66 | #if this snapshot is the last one: 67 | if orderbook.timestamp == 1656028781362646546: 68 | 69 | #while each of temp_bids and temp_asks is not none, 70 | #we may calculate pnl by comparing these two orders 71 | while len(self.temp_bids)*len(self.temp_asks) > 0: 72 | 73 | self.pnl += self.temp_asks[0] - self.temp_bids[0] 74 | self.temp_asks.pop(0) 75 | self.temp_bids.pop(0) 76 | 77 | #when one of these lists is none, 78 | #we execute other side aggressively 79 | #(sell or buy by orderbook price) 80 | if len(self.temp_asks) == 0: 81 | while len(self.temp_bids)>0: 82 | self.pnl += ((orderbook.asks[0]) - self.temp_bids[0])*self.size 83 | self.temp_bids.pop(0) 84 | else: 85 | while len(self.temp_asks)>0: 86 | self.pnl += ((-orderbook.bids[0]) + self.temp_asks[0])*self.size 87 | 88 | self.total_size = (len(self.temp_bids) - len(self.temp_asks))*self.size 89 | 90 | #we randomly choose the side (bid or ask) 91 | side_ = random.randint(0,1) 92 | 93 | #split own_trades on bids and asks 94 | for order_id, order in own_trades.copy().items(): 95 | 96 | if order.side == 'BID': 97 | self.temp_bids.append(order.price) 98 | 99 | elif order.side == 'ASK': 100 | self.temp_asks.append(order.price) 101 | 102 | own_trades.pop(order_id) 103 | 104 | while len(self.temp_bids)*len(self.temp_asks)>0: 105 | 106 | #we can calculate pnl only if we have bid AND ask executed orders 107 | self.pnl += (self.temp_asks[0] - self.temp_bids[0])*self.size 108 | self.temp_asks.pop(0) 109 | self.temp_bids.pop(0) 110 | 111 | #calculating the total size of inventory 112 | self.total_size = (len(self.temp_bids) - len(self.temp_asks))*self.size 113 | 114 | #max_position control 115 | #if total size MORE that max_position, then we can't buy more assets 116 | if (self.total_size <= self.max_position) and (side_ == 0): 117 | side = 'BID' 118 | price = orderbook.bids[0] 119 | sim.place_order(side, self.size, price, orderbook.timestamp) 120 | #if total size LESS than max_position*(-1), then we can't sell more assets 121 | elif (self.total_size >= self.max_position * (-1)) and (side_ == 1): 122 | side = 'ASK' 123 | price = orderbook.asks[0] 124 | sim.place_order(side, self.size, price, orderbook.timestamp) 125 | 126 | #cancel orders, that haven't exposed for t_0 127 | for order_id, order in orders_dict_latency.copy().items(): 128 | if (orderbook.timestamp - order.timestamp) >= (self.t_0)*1000000: 129 | sim.cancel_order(order_id) 130 | else: 131 | break 132 | self.pnl_data.append([self.pnl, orderbook.timestamp, self.total_size, orderbook.bids[0]]) 133 | except StopIteration: 134 | return self.pnl_data 135 | break 136 | 137 | 138 | 139 | def load_md_from_file(path: str) -> list[MdUpdate]: 140 | #load data 141 | btc_lobs = pd.read_csv(f'{path}/lobs.csv') 142 | btc_trades = pd.read_csv(f'{path}/trades.csv') 143 | 144 | #save only best bid/ask prices and vols 145 | btc_lobs = btc_lobs.set_index('receive_ts', drop=False) 146 | btc_lobs = btc_lobs.iloc[:, :6] 147 | btc_lobs.columns = [i.replace('btcusdt:Binance:LinearPerpetual_', '') for i in btc_lobs.columns] 148 | 149 | #merge data (because we have to different columns: 'receive_ts' and 'exchange_ts') 150 | md = btc_trades.groupby(by='exchange_ts').agg({'receive_ts': 'last', 'price': 'max', 'aggro_side': ['last', 'count']}) 151 | md.columns = ['_'.join(i) for i in md] 152 | df = pd.merge_asof(md, btc_lobs.iloc[:, 2:6], left_on='receive_ts_last', right_index=True) 153 | 154 | #make a slice of your data (optional) 155 | #df = df.iloc[:100000, :] 156 | 157 | #lists for shapshots of orderbook and trades 158 | orderbooks = [] 159 | trades = [] 160 | 161 | asks = df[['ask_price_0', 'ask_vol_0']].values 162 | bids = df[['bid_price_0', 'bid_vol_0']].values 163 | receive_ts_ = df['receive_ts_last'].values 164 | 165 | #append current orderbook/trade into orderbooks/trades 166 | for i in range(df.shape[0]): 167 | orderbook = OrderbookSnapshotUpdate(receive_ts_[i], asks[i], bids[i]) 168 | orderbooks.append(orderbook) 169 | trade = AnonTrade(receive_ts_[i], 0.001, df['price_max'].values[i], df['aggro_side_last'].values[i]) 170 | trades.append(trade) 171 | 172 | return orderbooks, trades 173 | 174 | 175 | class Sim: 176 | def __init__(self, execution_latency: int, md_latency: int) -> None: 177 | self.orderbooks, self.trades = load_md_from_file("..md/btcusdt_Binance_LinearPerpetual") 178 | self.orderbook = iter(self.orderbooks) 179 | self.trade = iter(self.trades) 180 | #execution_latency - delay due to the fact that orders are not placed on the market instantly 181 | self.execution_latency = execution_latency 182 | #md_latency - delay due to the fact that we don't receive information about successful trades instantly 183 | self.md_latency = md_latency 184 | self.orders_dict = {} #orders in queue for placing in orderbook 185 | self.orders_dict_latency = {} #orders that we are placing on exchange 186 | #(each order 've waited execution_latency time) 187 | self.own_trades = {} #successfully executed trades 188 | self.trade_id = 1 #ids for successfully executed trades 189 | self.order_id = 1 #ids for trades that are on exchange now 190 | 191 | #this function imitates real behaviour of orderbook on exchange 192 | #it returns what 've happened for one tick of orderbook 193 | def tick(self, maker_fee) -> MdUpdate: 194 | trade = next(self.trade) 195 | orderbook = next(self.orderbook) 196 | #if order 've waited for execution_latency time, it must be exposed on exchange 197 | for order_id, order in self.orders_dict.copy().items(): 198 | #.timestamp is in nanosecs, but latency is in ms, so we multiply latency by 10^6 199 | if (orderbook.timestamp - order.timestamp) >= (self.execution_latency)*1000000: 200 | self.prepare_orders(order_id, order) 201 | #delete our order from orders_dict (because now this order is on exchange) 202 | self.orders_dict.pop(order_id) 203 | 204 | own_trades = self.execute_orders(trade, orderbook, maker_fee) 205 | 206 | return orderbook, own_trades, self.orders_dict_latency 207 | 208 | #this function exposes current order on exchange 209 | #(this order is on exchange now) 210 | def prepare_orders(self, order_id, order): 211 | self.orders_dict_latency[order_id] = order 212 | 213 | return self.orders_dict_latency 214 | 215 | #this function executes our orders 216 | #(on specific conditions) 217 | def execute_orders(self, trade, orderbook, maker_fee): 218 | 219 | for order_id, order in self.orders_dict_latency.copy().items(): 220 | 221 | #checking specific conditions 222 | if (order.side == 'BID' and order.price >=orderbook.asks[0] + maker_fee) or \ 223 | (order.side == 'ASK' and order.price <= orderbook.bids[0] + maker_fee): 224 | 225 | own_trade_order = OwnTrade(trade.timestamp + (self.md_latency)*1000000, self.trade_id, order.order_id, order.size, order.price, order.side) 226 | self.own_trades[self.trade_id] = own_trade_order 227 | self.orders_dict_latency.pop(order_id) 228 | self.trade_id += 1 229 | 230 | elif (order.side == 'BID' and trade.side == 'ASK' and order.price >= trade.price + maker_fee) or \ 231 | (order.side == 'ASK' and trade.side == 'BID' and order.price <= trade.price + maker_fee): 232 | 233 | own_trade_order = OwnTrade(trade.timestamp + (self.md_latency)*1000000, self.trade_id, order.order_id, order.size, order.price, order.side) 234 | self.own_trades[self.trade_id] = own_trade_order 235 | self.orders_dict_latency.pop(order_id) 236 | self.trade_id += 1 237 | 238 | return self.own_trades 239 | 240 | #this function creates an exemplar of the Order class 241 | def place_order(self, side, size, price, timestamp): 242 | order = Order(self.order_id, size, price, timestamp, side) 243 | self.order_id += 1 244 | self.orders_dict[self.order_id] = order 245 | 246 | return self.orders_dict 247 | 248 | #this function cancels order due to specific conditions 249 | def cancel_order(self, order_id): 250 | self.orders_dict_latency.pop(order_id) 251 | 252 | 253 | if __name__ == "__main__": 254 | #create our strategy 255 | strategy = Strategy(0.001, 400, 0, []) 256 | #create out simulator 257 | sim = Sim(10, 10) 258 | #calculate our pnl 259 | pnl_data = strategy.run(sim) 260 | pnl_data = pd.DataFrame(pnl_data1) 261 | pnl_data.to_csv('pnl_data.csv') -------------------------------------------------------------------------------- /Stoikov-implentation/simulator.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from collections import deque 3 | from dataclasses import dataclass 4 | #from math import gamma 5 | #from signal import Sigmasks 6 | from typing import List, Optional, Tuple, Union, Deque, Dict 7 | import numpy as np 8 | from sortedcontainers import SortedDict 9 | 10 | @dataclass 11 | class Order: # Our own placed order 12 | place_ts : float # ts when we place the order 13 | exchange_ts : float # ts when exchange(simulator) get the order 14 | order_id: int 15 | side: str 16 | size: float 17 | price: float 18 | 19 | 20 | @dataclass 21 | class CancelOrder: 22 | exchange_ts: float 23 | id_to_delete : int 24 | 25 | @dataclass 26 | class AnonTrade: # Market trade 27 | exchange_ts : float 28 | receive_ts : float 29 | side: str 30 | size: float 31 | price: float 32 | 33 | 34 | @dataclass 35 | class OwnTrade: # Execution of own placed order 36 | place_ts : float # ts when we call place_order method, for debugging 37 | exchange_ts: float 38 | receive_ts: float 39 | trade_id: int 40 | order_id: int 41 | side: str 42 | size: float 43 | price: float 44 | execute : str # BOOK or TRADE 45 | 46 | 47 | def __post_init__(self): 48 | assert isinstance(self.side, str) 49 | 50 | @dataclass 51 | class OrderbookSnapshotUpdate: # Orderbook tick snapshot 52 | exchange_ts : float 53 | receive_ts : float 54 | asks: List[Tuple[float, float]] # tuple[price, size] 55 | bids: List[Tuple[float, float]] 56 | 57 | 58 | @dataclass 59 | class MdUpdate: # Data of a tick 60 | exchange_ts : float 61 | receive_ts : float 62 | orderbook: Optional[OrderbookSnapshotUpdate] = None 63 | trade: Optional[AnonTrade] = None 64 | 65 | 66 | def update_best_positions(best_bid:float, best_ask:float, md:MdUpdate) -> Tuple[float, float]: 67 | if not md.orderbook is None: 68 | best_bid = md.orderbook.bids[0][0] 69 | best_ask = md.orderbook.asks[0][0] 70 | 71 | return best_bid, best_ask 72 | 73 | 74 | class Sim: 75 | def __init__(self, market_data: List[MdUpdate], execution_latency: float, md_latency: float) -> None: 76 | ''' 77 | Args: 78 | market_data(List[MdUpdate]): market data 79 | execution_latency(float): latency in nanoseconds 80 | md_latency(float): latency in nanoseconds 81 | ''' 82 | self.inventory = 0 #our current inventory 83 | 84 | #transform md to queue 85 | self.md_queue = deque( market_data ) 86 | #action queue 87 | self.actions_queue:Deque[ Union[Order, CancelOrder] ] = deque() 88 | #SordetDict: receive_ts -> [updates] 89 | self.strategy_updates_queue = SortedDict() 90 | #map : order_id -> Order 91 | self.ready_to_execute_orders:Dict[int, Order] = {} 92 | 93 | #current md 94 | self.md:Optional[MdUpdate] = None 95 | #current ids 96 | self.order_id = 0 97 | self.trade_id = 0 98 | #latency 99 | self.latency = execution_latency 100 | self.md_latency = md_latency 101 | #current bid and ask 102 | self.best_bid = -np.inf 103 | self.best_ask = np.inf 104 | self.mid_price = 0 105 | #current trade 106 | self.trade_price = {} 107 | self.trade_price['BID'] = -np.inf 108 | self.trade_price['ASK'] = np.inf 109 | #last order 110 | self.last_order:Optional[Order] = None 111 | 112 | 113 | def get_md_queue_event_time(self) -> float: 114 | return np.inf if len(self.md_queue) == 0 else self.md_queue[0].exchange_ts 115 | 116 | 117 | def get_actions_queue_event_time(self) -> float: 118 | return np.inf if len(self.actions_queue) == 0 else self.actions_queue[0].exchange_ts 119 | 120 | 121 | def get_strategy_updates_queue_event_time(self) -> float: 122 | return np.inf if len(self.strategy_updates_queue) == 0 else self.strategy_updates_queue.keys()[0] 123 | 124 | 125 | def get_order_id(self) -> int: 126 | res = self.order_id 127 | self.order_id += 1 128 | return res 129 | 130 | 131 | def get_trade_id(self) -> int: 132 | res = self.trade_id 133 | self.trade_id += 1 134 | return res 135 | 136 | 137 | def update_best_pos(self) -> None: 138 | assert not self.md is None, "no current market data!" 139 | if not self.md.orderbook is None: 140 | self.best_bid = self.md.orderbook.bids[0][0] 141 | self.best_ask = self.md.orderbook.asks[0][0] 142 | #self.mid_price = (self.md.orderbook.bids[0][0] + self.md.orderbook.asks[0][0]) * 0.5 143 | 144 | 145 | def update_last_trade(self) -> None: 146 | assert not self.md is None, "no current market data!" 147 | if not self.md.trade is None: 148 | self.trade_price[self.md.trade.side] = self.md.trade.price 149 | 150 | 151 | def delete_last_trade(self) -> None: 152 | self.trade_price['BID'] = -np.inf 153 | self.trade_price['ASK'] = np.inf 154 | 155 | 156 | def update_md(self, md:MdUpdate) -> None: 157 | #current orderbook 158 | self.md = md 159 | #update position 160 | self.update_best_pos() 161 | #update info about last trade 162 | self.update_last_trade() 163 | 164 | #add md to strategy_updates_queue 165 | if not md.receive_ts in self.strategy_updates_queue.keys(): 166 | self.strategy_updates_queue[md.receive_ts] = [] 167 | self.strategy_updates_queue[md.receive_ts].append(md) 168 | 169 | 170 | def update_action(self, action:Union[Order, CancelOrder]) -> None: 171 | 172 | if isinstance(action, Order): 173 | #self.ready_to_execute_orders[action.order_id] = action 174 | #save last order to try to execute it aggressively 175 | self.last_order = action 176 | elif isinstance(action, CancelOrder): 177 | #cancel order 178 | if action.id_to_delete in self.ready_to_execute_orders: 179 | self.ready_to_execute_orders.pop(action.id_to_delete) 180 | else: 181 | assert False, "Wrong action type!" 182 | 183 | 184 | def tick(self) -> tuple([ float, float, List[ Union[OwnTrade, MdUpdate] ]]): 185 | ''' 186 | Simulation tick 187 | 188 | Returns: 189 | receive_ts(float): receive timestamp in nanoseconds 190 | res(List[Union[OwnTrade, MdUpdate]]): simulation result. 191 | ''' 192 | while True: 193 | #get event time for all the queues 194 | strategy_updates_queue_et = self.get_strategy_updates_queue_event_time() 195 | md_queue_et = self.get_md_queue_event_time() 196 | actions_queue_et = self.get_actions_queue_event_time() 197 | 198 | #if both queue are empty 199 | if md_queue_et == np.inf and actions_queue_et == np.inf: 200 | break 201 | 202 | #strategy queue has minimum event time 203 | if strategy_updates_queue_et < min(md_queue_et, actions_queue_et): 204 | break 205 | 206 | 207 | if md_queue_et <= actions_queue_et: 208 | self.update_md( self.md_queue.popleft() ) 209 | if actions_queue_et <= md_queue_et: 210 | self.update_action( self.actions_queue.popleft() ) 211 | 212 | #execute last order aggressively 213 | self.execute_last_order() 214 | #execute orders with current orderbook 215 | self.execute_orders() 216 | #delete last trade 217 | self.delete_last_trade() 218 | #end of simulation 219 | if len(self.strategy_updates_queue) == 0: 220 | return self.inventory, np.inf, None 221 | key = self.strategy_updates_queue.keys()[0] 222 | res = self.strategy_updates_queue.pop(key) 223 | 224 | #print( self.inventory, key, res) 225 | return self.inventory, key, res 226 | 227 | def execute_last_order(self) -> None: 228 | ''' 229 | this function tries to execute self.last order aggressively 230 | ''' 231 | #nothing to execute 232 | if self.last_order is None: 233 | return 234 | 235 | executed_price, execute = None, None 236 | # 237 | if self.last_order.side == 'BID' and self.last_order.price >= self.best_ask: 238 | executed_price = self.best_ask 239 | execute = 'BOOK' 240 | # 241 | elif self.last_order.side == 'ASK' and self.last_order.price <= self.best_bid: 242 | executed_price = self.best_bid 243 | execute = 'BOOK' 244 | 245 | if not executed_price is None: 246 | executed_order = OwnTrade( 247 | self.last_order.place_ts, # when we place the order 248 | self.md.exchange_ts, #exchange ts 249 | self.md.exchange_ts + self.md_latency, #receive ts 250 | self.get_trade_id(), #trade id 251 | self.last_order.order_id, 252 | self.last_order.side, 253 | self.last_order.size, 254 | executed_price, execute) 255 | #add order to strategy update queue 256 | #there is no defaultsorteddict so I have to do this 257 | if not executed_order.receive_ts in self.strategy_updates_queue: 258 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 259 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 260 | else: 261 | self.ready_to_execute_orders[self.last_order.order_id] = self.last_order 262 | 263 | #delete last order 264 | self.last_order = None 265 | 266 | 267 | def execute_orders(self) -> None: 268 | executed_orders_id = [] 269 | for order_id, order in self.ready_to_execute_orders.items(): 270 | 271 | executed_price, execute = None, None 272 | 273 | # 274 | if order.side == 'BID' and order.price >= self.best_ask: 275 | executed_price = order.price 276 | execute = 'BOOK' 277 | # 278 | elif order.side == 'ASK' and order.price <= self.best_bid: 279 | executed_price = order.price 280 | execute = 'BOOK' 281 | # 282 | elif order.side == 'BID' and order.price >= self.trade_price['ASK']: 283 | executed_price = order.price 284 | execute = 'TRADE' 285 | # 286 | elif order.side == 'ASK' and order.price <= self.trade_price['BID']: 287 | executed_price = order.price 288 | execute = 'TRADE' 289 | 290 | if not executed_price is None: 291 | executed_order = OwnTrade( 292 | order.place_ts, # when we place the order 293 | self.md.exchange_ts, #exchange ts 294 | self.md.exchange_ts + self.md_latency, #receive ts 295 | self.get_trade_id(), #trade id 296 | order_id, order.side, order.size, executed_price, execute) 297 | 298 | executed_orders_id.append(order_id) 299 | 300 | #added order to strategy update queue 301 | #there is no defaultsorteddict so i have to do this 302 | if not executed_order.receive_ts in self.strategy_updates_queue: 303 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 304 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 305 | if executed_order.side == 'BID': 306 | self.inventory += executed_order.size 307 | elif executed_order.side == 'ASK': 308 | self.inventory -= executed_order.size 309 | 310 | #deleting executed orders 311 | for k in executed_orders_id: 312 | self.ready_to_execute_orders.pop(k) 313 | 314 | 315 | def place_order(self, ts:float, size:float, side:str, price:float) -> Order: 316 | order = Order(ts, ts + self.latency, self.get_order_id(), side, size, price) 317 | self.actions_queue.append(order) 318 | return order 319 | 320 | 321 | def cancel_order(self, ts:float, id_to_delete:int) -> CancelOrder: 322 | ts += self.latency 323 | delete_order = CancelOrder(ts, id_to_delete) 324 | self.actions_queue.append(delete_order) 325 | return delete_order 326 | 327 | #-------------------STRATEGY------------------------- 328 | class BestPosStrategy: 329 | ''' 330 | This strategy places ask and bid order every `delay` nanoseconds. 331 | If the order has not been executed within `hold_time` nanoseconds, it is canceled. 332 | ''' 333 | def __init__(self, delay: float, sigma: float, gamma: float, k: float, hold_time:Optional[float]) -> None: 334 | ''' 335 | Args: 336 | delay(float): delay between orders in nanoseconds 337 | sigma: volatility 338 | gamma: attitude to risk 339 | k: arrival rate 340 | hold_time(Optional[float]): holding time in nanoseconds 341 | 342 | ''' 343 | self.delay = delay 344 | if hold_time is None: 345 | hold_time = max( delay * 5, pd.Timedelta(10, 's').delta ) 346 | self.hold_time = hold_time 347 | 348 | self.sigma = sigma #volatility 349 | self.gamma = gamma #attitude to risk 350 | self.k = k #arrival rate of orders as the change in volume per second 351 | #we use it to normalize time 352 | self.T_begin = 1655942402250125991 #beginning of our session 353 | self.T_end = 1655943545128514890 #end of our session 354 | 355 | 356 | def run(self, sim: Sim ) ->\ 357 | Tuple[ List[OwnTrade], List[MdUpdate], List[ Union[OwnTrade, MdUpdate] ], List[Order] ]: 358 | ''' 359 | This function runs simulation 360 | 361 | Args: 362 | sim(Sim): simulator 363 | Returns: 364 | trades_list(List[OwnTrade]): list of our executed trades 365 | md_list(List[MdUpdate]): list of market data received by strategy 366 | updates_list( List[ Union[OwnTrade, MdUpdate] ] ): list of all updates 367 | received by strategy(market data and information about executed trades) 368 | all_orders(List[Orted]): list of all placed orders 369 | ''' 370 | 371 | #market data list 372 | md_list:List[MdUpdate] = [] 373 | #executed trades list 374 | trades_list:List[OwnTrade] = [] 375 | #all updates list 376 | updates_list = [] 377 | #current best positions 378 | best_bid = -np.inf 379 | best_ask = np.inf 380 | 381 | #last order timestamp 382 | prev_time = -np.inf 383 | #orders that have not been executed/canceled yet 384 | ongoing_orders: Dict[int, Order] = {} 385 | all_orders = [] 386 | while True: 387 | #get update from simulator 388 | inventory, receive_ts, updates = sim.tick() 389 | if updates is None: 390 | break 391 | #save updates 392 | updates_list += updates 393 | for update in updates: 394 | #update best position 395 | if isinstance(update, MdUpdate): 396 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 397 | md_list.append(update) 398 | elif isinstance(update, OwnTrade): 399 | trades_list.append(update) 400 | #delete executed trades from the dict 401 | if update.order_id in ongoing_orders.keys(): 402 | ongoing_orders.pop(update.order_id) 403 | else: 404 | assert False, 'invalid type of update!' 405 | 406 | if receive_ts - prev_time >= self.delay: 407 | prev_time = receive_ts 408 | #place order 409 | #calculate the mid-price 410 | mid = 0.5 * (best_bid + best_ask) 411 | #normalize time 412 | T_t = 1 - (receive_ts - self.T_begin)/(self.T_end - self.T_begin) 413 | #calculate difference between reservation price and mid price 414 | mid_reservation_spread = self.gamma * self.sigma * self.sigma * inventory * T_t 415 | #calculate agent's spread 416 | spread = self.gamma * self.sigma * self.sigma * T_t + (2/self.gamma) * np.log(1 + (self.gamma/self.k)) 417 | #calculate current ask/bid prices 418 | ask_price = mid + 0.5 * (spread - mid_reservation_spread) 419 | bid_price = mid - 0.5 * (spread + mid_reservation_spread) 420 | 421 | bid_order = sim.place_order( receive_ts, 0.001, 'BID', bid_price ) #подредачить 422 | ask_order = sim.place_order( receive_ts, 0.001, 'ASK', ask_price ) #подредачить 423 | ongoing_orders[bid_order.order_id] = bid_order 424 | ongoing_orders[ask_order.order_id] = ask_order 425 | 426 | all_orders += [bid_order, ask_order] 427 | 428 | to_cancel = [] 429 | for ID, order in ongoing_orders.items(): 430 | if order.place_ts < receive_ts - self.hold_time: 431 | sim.cancel_order( receive_ts, ID ) 432 | to_cancel.append(ID) 433 | for ID in to_cancel: 434 | ongoing_orders.pop(ID) 435 | 436 | 437 | return trades_list, md_list, updates_list, all_orders 438 | #--------------------------------------------------------LOADDATA---------------------------- 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | def load_trades(path, nrows=10000) -> List[AnonTrade]: 447 | ''' 448 | This function downloads trades data 449 | 450 | Args: 451 | path(str): path to file 452 | nrows(int): number of rows to read 453 | 454 | Return: 455 | trades(List[AnonTrade]): list of trades 456 | ''' 457 | trades = pd.read_csv(path + 'trades.csv', nrows=nrows) 458 | #trades = trades.iloc[:10000, :] 459 | trades = trades[ ['exchange_ts', 'receive_ts', 'aggro_side', 'size', 'price' ] ].sort_values(["exchange_ts", 'receive_ts']) 460 | receive_ts = trades.receive_ts.values 461 | exchange_ts = trades.exchange_ts.values 462 | trades = [ AnonTrade(*args) for args in trades.values] 463 | return trades 464 | 465 | 466 | def load_books(path, nrows=10000) -> List[OrderbookSnapshotUpdate]: 467 | ''' 468 | This function downloads orderbook market data 469 | 470 | Args: 471 | path(str): path to file 472 | nrows(int): number of rows to read 473 | 474 | Return: 475 | books(List[OrderbookSnapshotUpdate]): list of orderbooks snapshots 476 | ''' 477 | lobs = pd.read_csv(path + 'lobs.csv', nrows=nrows) 478 | #lobs = lobs.iloc[:10000, :] 479 | #rename columns 480 | names = lobs.columns.values 481 | ln = len('btcusdt:Binance:LinearPerpetual_') 482 | renamer = { name:name[ln:] for name in names[2:]} 483 | renamer[' exchange_ts'] = 'exchange_ts' 484 | lobs.rename(renamer, axis=1, inplace=True) 485 | 486 | #timestamps 487 | receive_ts = lobs.receive_ts.values 488 | exchange_ts = lobs.exchange_ts.values 489 | #list of ask_price, ask_vol for different levels of orderbook 490 | #sizes: len(asks) = 10, len(asks[0]) = len(lobs) 491 | asks = [list(zip(lobs[f"ask_price_{i}"],lobs[f"ask_vol_{i}"])) for i in range(10)] 492 | #transpose the list 493 | asks = [ [asks[i][j] for i in range(len(asks))] for j in range(len(asks[0]))] 494 | #transpose the list 495 | bids = [list(zip(lobs[f"bid_price_{i}"],lobs[f"bid_vol_{i}"])) for i in range(10)] 496 | bids = [ [bids[i][j] for i in range(len(bids))] for j in range(len(bids[0]))] 497 | 498 | books = list( OrderbookSnapshotUpdate(*args) for args in zip(exchange_ts, receive_ts, asks, bids) ) 499 | return books 500 | 501 | 502 | def merge_books_and_trades(books : List[OrderbookSnapshotUpdate], trades: List[AnonTrade]) -> List[MdUpdate]: 503 | ''' 504 | This function merges lists of orderbook snapshots and trades 505 | ''' 506 | trades_dict = { (trade.exchange_ts, trade.receive_ts) : trade for trade in trades } 507 | books_dict = { (book.exchange_ts, book.receive_ts) : book for book in books } 508 | 509 | ts = sorted(trades_dict.keys() | books_dict.keys()) 510 | 511 | md = [MdUpdate(*key, books_dict.get(key, None), trades_dict.get(key, None)) for key in ts] 512 | return md 513 | 514 | 515 | def load_md_from_file(path: str, nrows=10000) -> List[MdUpdate]: 516 | ''' 517 | This function downloads orderbooks ans trades and merges them 518 | ''' 519 | books = load_books(path, nrows) 520 | trades = load_trades(path, nrows) 521 | return merge_books_and_trades(books, trades) 522 | 523 | #------------------------------GETPNL--------------------------- 524 | 525 | def get_pnl(updates_list:List[ Union[MdUpdate, OwnTrade] ]) -> pd.DataFrame: 526 | ''' 527 | This function calculates PnL from list of updates 528 | ''' 529 | 530 | #current position in btc and usd 531 | btc_pos, usd_pos = 0.0, 0.0 532 | 533 | N = len(updates_list) 534 | btc_pos_arr = np.zeros((N, )) 535 | usd_pos_arr = np.zeros((N, )) 536 | mid_price_arr = np.zeros((N, )) 537 | #current best_bid and best_ask 538 | best_bid:float = -np.inf 539 | best_ask:float = np.inf 540 | 541 | for i, update in enumerate(updates_list): 542 | 543 | if isinstance(update, MdUpdate): 544 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 545 | #mid price 546 | #i use it to calculate current portfolio value 547 | mid_price = 0.5 * ( best_ask + best_bid ) 548 | 549 | if isinstance(update, OwnTrade): 550 | trade = update 551 | #update positions 552 | if trade.side == 'BID': 553 | btc_pos += trade.size 554 | usd_pos -= trade.price * trade.size 555 | elif trade.side == 'ASK': 556 | btc_pos -= trade.size 557 | usd_pos += trade.price * trade.size 558 | #current portfolio value 559 | 560 | btc_pos_arr[i] = btc_pos 561 | usd_pos_arr[i] = usd_pos 562 | mid_price_arr[i] = mid_price 563 | 564 | worth_arr = btc_pos_arr * mid_price_arr + usd_pos_arr 565 | receive_ts = [update.receive_ts for update in updates_list] 566 | exchange_ts = [update.exchange_ts for update in updates_list] 567 | 568 | df = pd.DataFrame({"exchange_ts": exchange_ts, "receive_ts":receive_ts, "total":worth_arr, "BTC":btc_pos_arr, 569 | "USD":usd_pos_arr, "mid_price":mid_price_arr}) 570 | df = df.groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 571 | return df 572 | 573 | 574 | def trade_to_dataframe(trades_list:List[OwnTrade]) -> pd.DataFrame: 575 | exchange_ts = [ trade.exchange_ts for trade in trades_list ] 576 | receive_ts = [ trade.receive_ts for trade in trades_list ] 577 | 578 | size = [ trade.size for trade in trades_list ] 579 | price = [ trade.price for trade in trades_list ] 580 | side = [trade.side for trade in trades_list ] 581 | 582 | dct = { 583 | "exchange_ts" : exchange_ts, 584 | "receive_ts" : receive_ts, 585 | "size" : size, 586 | "price" : price, 587 | "side" : side 588 | } 589 | 590 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 591 | return df 592 | 593 | 594 | def md_to_dataframe(md_list: List[MdUpdate]) -> pd.DataFrame: 595 | 596 | best_bid = -np.inf 597 | best_ask = np.inf 598 | best_bids = [] 599 | best_asks = [] 600 | for md in md_list: 601 | best_bid, best_ask = update_best_positions(best_bid, best_ask, md) 602 | best_bids.append(best_bid) 603 | best_asks.append(best_ask) 604 | 605 | exchange_ts = [ md.exchange_ts for md in md_list ] 606 | receive_ts = [ md.receive_ts for md in md_list ] 607 | dct = { 608 | "exchange_ts" : exchange_ts, 609 | "receive_ts" :receive_ts, 610 | "bid_price" : best_bids, 611 | "ask_price" : best_asks 612 | } 613 | 614 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 615 | return df 616 | 617 | if __name__ == "__main__": 618 | PATH_TO_FILE = '...' 619 | NROWS = 30000 620 | md = load_md_from_file(path=PATH_TO_FILE, nrows=NROWS) 621 | latency = pd.Timedelta(10, 'ms').delta 622 | md_latency = pd.Timedelta(10, 'ms').delta 623 | sim = Sim(md, latency, md_latency) 624 | delay = pd.Timedelta(0.1, 's').delta 625 | hold_time = pd.Timedelta(10, 's').delta 626 | strategy = BestPosStrategy(delay, 4.0, 0.5, 2, hold_time) 627 | trades_list, md_list, updates_list, all_orders = strategy.run(sim) 628 | df = get_pnl(updates_list) 629 | df.to_csv('pnl01.csv') 630 | -------------------------------------------------------------------------------- /comparison of Avellaneda-Stoikov and Gueant models/Avellaneda_Stoikov_simulator.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from collections import deque 3 | from dataclasses import dataclass 4 | #from math import gamma 5 | #from signal import Sigmasks 6 | from typing import List, Optional, Tuple, Union, Deque, Dict 7 | import numpy as np 8 | from sortedcontainers import SortedDict 9 | 10 | @dataclass 11 | class Order: # Our own placed order 12 | place_ts : float # ts when we place the order 13 | exchange_ts : float # ts when exchange(simulator) get the order 14 | order_id: int 15 | side: str 16 | size: float 17 | price: float 18 | 19 | 20 | @dataclass 21 | class CancelOrder: 22 | exchange_ts: float 23 | id_to_delete : int 24 | 25 | @dataclass 26 | class AnonTrade: # Market trade 27 | exchange_ts : float 28 | receive_ts : float 29 | side: str 30 | size: float 31 | price: float 32 | 33 | 34 | @dataclass 35 | class OwnTrade: # Execution of own placed order 36 | place_ts : float # ts when we call place_order method, for debugging 37 | exchange_ts: float 38 | receive_ts: float 39 | trade_id: int 40 | order_id: int 41 | side: str 42 | size: float 43 | price: float 44 | execute : str # BOOK or TRADE 45 | 46 | 47 | def __post_init__(self): 48 | assert isinstance(self.side, str) 49 | 50 | @dataclass 51 | class OrderbookSnapshotUpdate: # Orderbook tick snapshot 52 | exchange_ts : float 53 | receive_ts : float 54 | asks: List[Tuple[float, float]] # tuple[price, size] 55 | bids: List[Tuple[float, float]] 56 | 57 | 58 | @dataclass 59 | class MdUpdate: # Data of a tick 60 | exchange_ts : float 61 | receive_ts : float 62 | orderbook: Optional[OrderbookSnapshotUpdate] = None 63 | trade: Optional[AnonTrade] = None 64 | 65 | 66 | def update_best_positions(best_bid:float, best_ask:float, md:MdUpdate) -> Tuple[float, float]: 67 | if not md.orderbook is None: 68 | best_bid = md.orderbook.bids[0][0] 69 | best_ask = md.orderbook.asks[0][0] 70 | 71 | return best_bid, best_ask 72 | 73 | 74 | class Sim: 75 | def __init__(self, market_data: List[MdUpdate], execution_latency: float, md_latency: float) -> None: 76 | ''' 77 | Args: 78 | market_data(List[MdUpdate]): market data 79 | execution_latency(float): latency in nanoseconds 80 | md_latency(float): latency in nanoseconds 81 | ''' 82 | self.inventory = 0 #our current inventory 83 | 84 | #transform md to queue 85 | self.md_queue = deque( market_data ) 86 | #action queue 87 | self.actions_queue:Deque[ Union[Order, CancelOrder] ] = deque() 88 | #SordetDict: receive_ts -> [updates] 89 | self.strategy_updates_queue = SortedDict() 90 | #map : order_id -> Order 91 | self.ready_to_execute_orders:Dict[int, Order] = {} 92 | 93 | #current md 94 | self.md:Optional[MdUpdate] = None 95 | #current ids 96 | self.order_id = 0 97 | self.trade_id = 0 98 | #latency 99 | self.latency = execution_latency 100 | self.md_latency = md_latency 101 | #current bid and ask 102 | self.best_bid = -np.inf 103 | self.best_ask = np.inf 104 | self.mid_price = 0 105 | #current trade 106 | self.trade_price = {} 107 | self.trade_price['BID'] = -np.inf 108 | self.trade_price['ASK'] = np.inf 109 | #last order 110 | self.last_order:Optional[Order] = None 111 | 112 | 113 | def get_md_queue_event_time(self) -> float: 114 | return np.inf if len(self.md_queue) == 0 else self.md_queue[0].exchange_ts 115 | 116 | 117 | def get_actions_queue_event_time(self) -> float: 118 | return np.inf if len(self.actions_queue) == 0 else self.actions_queue[0].exchange_ts 119 | 120 | 121 | def get_strategy_updates_queue_event_time(self) -> float: 122 | return np.inf if len(self.strategy_updates_queue) == 0 else self.strategy_updates_queue.keys()[0] 123 | 124 | 125 | def get_order_id(self) -> int: 126 | res = self.order_id 127 | self.order_id += 1 128 | return res 129 | 130 | 131 | def get_trade_id(self) -> int: 132 | res = self.trade_id 133 | self.trade_id += 1 134 | return res 135 | 136 | 137 | def update_best_pos(self) -> None: 138 | assert not self.md is None, "no current market data!" 139 | if not self.md.orderbook is None: 140 | self.best_bid = self.md.orderbook.bids[0][0]#МЭЭЭЭЭЭЭЭЭЭЭЭЭ 141 | self.best_ask = self.md.orderbook.asks[0][0]#"MUUUUUUUUUUUUUUU" 142 | #self.mid_price = (self.md.orderbook.bids[0][0] + self.md.orderbook.asks[0][0]) * 0.5 143 | 144 | 145 | def update_last_trade(self) -> None: 146 | assert not self.md is None, "no current market data!" 147 | if not self.md.trade is None: 148 | self.trade_price[self.md.trade.side] = self.md.trade.price 149 | 150 | 151 | def delete_last_trade(self) -> None: 152 | self.trade_price['BID'] = -np.inf 153 | self.trade_price['ASK'] = np.inf 154 | 155 | 156 | def update_md(self, md:MdUpdate) -> None: 157 | #current orderbook 158 | self.md = md 159 | #update position 160 | self.update_best_pos() 161 | #update info about last trade 162 | self.update_last_trade() 163 | 164 | #add md to strategy_updates_queue 165 | if not md.receive_ts in self.strategy_updates_queue.keys(): 166 | self.strategy_updates_queue[md.receive_ts] = [] 167 | self.strategy_updates_queue[md.receive_ts].append(md) 168 | 169 | 170 | def update_action(self, action:Union[Order, CancelOrder]) -> None: 171 | 172 | if isinstance(action, Order): 173 | #self.ready_to_execute_orders[action.order_id] = action 174 | #save last order to try to execute it aggressively 175 | self.last_order = action 176 | elif isinstance(action, CancelOrder): 177 | #cancel order 178 | if action.id_to_delete in self.ready_to_execute_orders: 179 | self.ready_to_execute_orders.pop(action.id_to_delete) 180 | else: 181 | assert False, "Wrong action type!" 182 | 183 | 184 | def tick(self) -> tuple([ float, float, List[ Union[OwnTrade, MdUpdate] ]]): 185 | ''' 186 | Simulation tick 187 | 188 | Returns: 189 | receive_ts(float): receive timestamp in nanoseconds 190 | res(List[Union[OwnTrade, MdUpdate]]): simulation result. 191 | ''' 192 | while True: 193 | #get event time for all the queues 194 | strategy_updates_queue_et = self.get_strategy_updates_queue_event_time() 195 | md_queue_et = self.get_md_queue_event_time() 196 | actions_queue_et = self.get_actions_queue_event_time() 197 | 198 | #if both queue are empty 199 | if md_queue_et == np.inf and actions_queue_et == np.inf: 200 | break 201 | 202 | #strategy queue has minimum event time 203 | if strategy_updates_queue_et < min(md_queue_et, actions_queue_et): 204 | break 205 | 206 | 207 | if md_queue_et <= actions_queue_et: 208 | self.update_md( self.md_queue.popleft() ) 209 | if actions_queue_et <= md_queue_et: 210 | self.update_action( self.actions_queue.popleft() ) 211 | 212 | #execute last order aggressively 213 | self.execute_last_order() 214 | #execute orders with current orderbook 215 | self.execute_orders() 216 | #delete last trade 217 | self.delete_last_trade() 218 | #end of simulation 219 | if len(self.strategy_updates_queue) == 0: 220 | return self.inventory, np.inf, None 221 | key = self.strategy_updates_queue.keys()[0] 222 | res = self.strategy_updates_queue.pop(key) 223 | 224 | #print( self.inventory, key, res) 225 | return self.inventory, key, res 226 | 227 | def execute_last_order(self) -> None: 228 | ''' 229 | this function tries to execute self.last order aggressively 230 | ''' 231 | #nothing to execute 232 | if self.last_order is None: 233 | return 234 | 235 | executed_price, execute = None, None 236 | # 237 | if self.last_order.side == 'BID' and self.last_order.price >= self.best_ask: 238 | executed_price = self.best_ask 239 | execute = 'BOOK' 240 | # 241 | elif self.last_order.side == 'ASK' and self.last_order.price <= self.best_bid: 242 | executed_price = self.best_bid 243 | execute = 'BOOK' 244 | 245 | if not executed_price is None: 246 | executed_order = OwnTrade( 247 | self.last_order.place_ts, # when we place the order 248 | self.md.exchange_ts, #exchange ts 249 | self.md.exchange_ts + self.md_latency, #receive ts 250 | self.get_trade_id(), #trade id 251 | self.last_order.order_id, 252 | self.last_order.side, 253 | self.last_order.size, 254 | executed_price, execute) 255 | #add order to strategy update queue 256 | #there is no defaultsorteddict so I have to do this 257 | if not executed_order.receive_ts in self.strategy_updates_queue: 258 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 259 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 260 | else: 261 | self.ready_to_execute_orders[self.last_order.order_id] = self.last_order 262 | 263 | #delete last order 264 | self.last_order = None 265 | 266 | 267 | def execute_orders(self) -> None: 268 | executed_orders_id = [] 269 | for order_id, order in self.ready_to_execute_orders.items(): 270 | 271 | executed_price, execute = None, None 272 | 273 | # 274 | if order.side == 'BID' and order.price >= self.best_ask: 275 | executed_price = order.price 276 | execute = 'BOOK' 277 | # 278 | elif order.side == 'ASK' and order.price <= self.best_bid: 279 | executed_price = order.price 280 | execute = 'BOOK' 281 | # 282 | elif order.side == 'BID' and order.price >= self.trade_price['ASK']: 283 | executed_price = order.price 284 | execute = 'TRADE' 285 | # 286 | elif order.side == 'ASK' and order.price <= self.trade_price['BID']: 287 | executed_price = order.price 288 | execute = 'TRADE' 289 | 290 | if not executed_price is None: 291 | executed_order = OwnTrade( 292 | order.place_ts, # when we place the order 293 | self.md.exchange_ts, #exchange ts 294 | self.md.exchange_ts + self.md_latency, #receive ts 295 | self.get_trade_id(), #trade id 296 | order_id, order.side, order.size, executed_price, execute) 297 | 298 | executed_orders_id.append(order_id) 299 | 300 | #added order to strategy update queue 301 | #there is no defaultsorteddict so i have to do this 302 | if not executed_order.receive_ts in self.strategy_updates_queue: 303 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 304 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 305 | if executed_order.side == 'BID': 306 | self.inventory += executed_order.size 307 | elif executed_order.side == 'ASK': 308 | self.inventory -= executed_order.size 309 | 310 | #deleting executed orders 311 | for k in executed_orders_id: 312 | self.ready_to_execute_orders.pop(k) 313 | 314 | 315 | def place_order(self, ts:float, size:float, side:str, price:float) -> Order: 316 | #добавляем заявку в список всех заявок 317 | order = Order(ts, ts + self.latency, self.get_order_id(), side, size, price) 318 | self.actions_queue.append(order) 319 | return order 320 | 321 | 322 | def cancel_order(self, ts:float, id_to_delete:int) -> CancelOrder: 323 | #добавляем заявку на удаление 324 | ts += self.latency 325 | delete_order = CancelOrder(ts, id_to_delete) 326 | self.actions_queue.append(delete_order) 327 | return delete_order 328 | 329 | #-------------------STRATEGY------------------------- 330 | class BestPosStrategy: 331 | ''' 332 | This strategy places ask and bid order every `delay` nanoseconds. 333 | If the order has not been executed within `hold_time` nanoseconds, it is canceled. 334 | ''' 335 | def __init__(self, delay: float, sigma: float, gamma: float, k: float, hold_time:Optional[float]) -> None: 336 | ''' 337 | Args: 338 | delay(float): delay between orders in nanoseconds 339 | sigma: volatility 340 | gamma: attitude to risk 341 | k: arrival rate 342 | hold_time(Optional[float]): holding time in nanoseconds 343 | 344 | ''' 345 | self.delay = delay 346 | if hold_time is None: 347 | hold_time = max( delay * 5, pd.Timedelta(10, 's').delta ) 348 | self.hold_time = hold_time 349 | 350 | self.sigma = sigma #volatility 351 | self.gamma = gamma #attitude to risk 352 | self.k = k #arrival rate of orders as the change in volume per second 353 | #we use it to normalize time 354 | self.T_begin = 1655953200999785662 #beginning of our session 355 | 356 | self.T_end = 1655974797846439628 #end of our session 357 | self.q = [] 358 | self.spread = [] 359 | 360 | 361 | def run(self, sim: Sim ) ->\ 362 | Tuple[ List[OwnTrade], List[MdUpdate], List[ Union[OwnTrade, MdUpdate] ], List[Order] ]: 363 | ''' 364 | This function runs simulation 365 | 366 | Args: 367 | sim(Sim): simulator 368 | Returns: 369 | trades_list(List[OwnTrade]): list of our executed trades 370 | md_list(List[MdUpdate]): list of market data received by strategy 371 | updates_list( List[ Union[OwnTrade, MdUpdate] ] ): list of all updates 372 | received by strategy(market data and information about executed trades) 373 | all_orders(List[Orted]): list of all placed orders 374 | ''' 375 | 376 | #market data list 377 | md_list:List[MdUpdate] = [] 378 | #executed trades list 379 | trades_list:List[OwnTrade] = [] 380 | #all updates list 381 | updates_list = [] 382 | #current best positions 383 | best_bid = -np.inf 384 | best_ask = np.inf 385 | 386 | #last order timestamp 387 | prev_time = -np.inf 388 | #orders that have not been executed/canceled yet 389 | ongoing_orders: Dict[int, Order] = {} 390 | all_orders = [] 391 | while True: 392 | #get update from simulator 393 | inventory, receive_ts, updates = sim.tick() 394 | self.q.append([inventory, receive_ts]) 395 | if updates is None: 396 | break 397 | #save updates 398 | updates_list += updates 399 | for update in updates: 400 | #update best position 401 | if isinstance(update, MdUpdate): 402 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 403 | md_list.append(update) 404 | elif isinstance(update, OwnTrade): 405 | trades_list.append(update) 406 | #delete executed trades from the dict 407 | if update.order_id in ongoing_orders.keys(): 408 | ongoing_orders.pop(update.order_id) 409 | else: 410 | assert False, 'invalid type of update!' 411 | 412 | if receive_ts - prev_time >= self.delay: 413 | prev_time = receive_ts 414 | #place order 415 | #calculate the mid-price 416 | mid = 0.5 * (best_bid + best_ask) 417 | #normalize time 418 | T_t = 1 - (receive_ts - self.T_begin)/(self.T_end - self.T_begin) 419 | #calculate difference between reservation price and mid price 420 | mid_reservation_spread = self.gamma * self.sigma * self.sigma * inventory * T_t 421 | #calculate agent's spread 422 | spread = self.gamma * self.sigma * self.sigma * T_t + (2/self.gamma) * np.log(1 + (self.gamma/self.k)) 423 | #calculate current ask/bid prices 424 | ask_price = mid - mid_reservation_spread + 0.5 * spread 425 | bid_price = mid - mid_reservation_spread - 0.5 * spread 426 | self.spread.append([ask_price, bid_price, receive_ts]) 427 | bid_order = sim.place_order( receive_ts, 0.001, 'BID', bid_price ) #подредачить 428 | ask_order = sim.place_order( receive_ts, 0.001, 'ASK', ask_price ) #подредачить 429 | ongoing_orders[bid_order.order_id] = bid_order 430 | ongoing_orders[ask_order.order_id] = ask_order 431 | 432 | all_orders += [bid_order, ask_order] 433 | 434 | to_cancel = [] 435 | for ID, order in ongoing_orders.items(): 436 | if order.place_ts < receive_ts - self.hold_time: 437 | sim.cancel_order( receive_ts, ID ) 438 | to_cancel.append(ID) 439 | for ID in to_cancel: 440 | ongoing_orders.pop(ID) 441 | 442 | 443 | return trades_list, md_list, updates_list, all_orders, self.q, self.spread 444 | #--------------------------------------------------------LOADDATA---------------------------- 445 | 446 | 447 | def load_trades(path, nrows=10000) -> List[AnonTrade]: 448 | ''' 449 | This function downloads trades data 450 | 451 | Args: 452 | path(str): path to file 453 | nrows(int): number of rows to read 454 | 455 | Return: 456 | trades(List[AnonTrade]): list of trades 457 | ''' 458 | trades = pd.read_csv(path + 'trades.csv', nrows=nrows) 459 | trades = trades.loc[719000:1835700, :] 460 | #переставляю колонки, чтобы удобнее подавать их в конструктор AnonTrade 461 | trades = trades[ ['exchange_ts', 'receive_ts', 'aggro_side', 'size', 'price' ] ].sort_values(["exchange_ts", 'receive_ts']) 462 | receive_ts = trades.receive_ts.values 463 | exchange_ts = trades.exchange_ts.values 464 | trades = [ AnonTrade(*args) for args in trades.values] 465 | return trades 466 | 467 | 468 | def load_books(path, nrows=10000) -> List[OrderbookSnapshotUpdate]: 469 | ''' 470 | This function downloads orderbook market data 471 | 472 | Args: 473 | path(str): path to file 474 | nrows(int): number of rows to read 475 | 476 | Return: 477 | books(List[OrderbookSnapshotUpdate]): list of orderbooks snapshots 478 | ''' 479 | lobs = pd.read_csv(path + 'lobs.csv', nrows=nrows) 480 | lobs = lobs.loc[314000:957000, :] 481 | #lobs = lobs.iloc[:10000, :] 482 | #rename columns 483 | names = lobs.columns.values 484 | ln = len('btcusdt:Binance:LinearPerpetual_') 485 | renamer = { name:name[ln:] for name in names[2:]} 486 | renamer[' exchange_ts'] = 'exchange_ts' 487 | lobs.rename(renamer, axis=1, inplace=True) 488 | 489 | #timestamps 490 | receive_ts = lobs.receive_ts.values 491 | exchange_ts = lobs.exchange_ts.values 492 | #список ask_price, ask_vol для разных уровней стакана 493 | #размеры: len(asks) = 10, len(asks[0]) = len(lobs) 494 | asks = [list(zip(lobs[f"ask_price_{i}"],lobs[f"ask_vol_{i}"])) for i in range(10)] 495 | #транспонируем список 496 | asks = [ [asks[i][j] for i in range(len(asks))] for j in range(len(asks[0]))] 497 | #тоже самое с бидами 498 | bids = [list(zip(lobs[f"bid_price_{i}"],lobs[f"bid_vol_{i}"])) for i in range(10)] 499 | bids = [ [bids[i][j] for i in range(len(bids))] for j in range(len(bids[0]))] 500 | 501 | books = list( OrderbookSnapshotUpdate(*args) for args in zip(exchange_ts, receive_ts, asks, bids) ) 502 | return books 503 | 504 | 505 | def merge_books_and_trades(books : List[OrderbookSnapshotUpdate], trades: List[AnonTrade]) -> List[MdUpdate]: 506 | ''' 507 | This function merges lists of orderbook snapshots and trades 508 | ''' 509 | trades_dict = { (trade.exchange_ts, trade.receive_ts) : trade for trade in trades } 510 | books_dict = { (book.exchange_ts, book.receive_ts) : book for book in books } 511 | 512 | ts = sorted(trades_dict.keys() | books_dict.keys()) 513 | 514 | md = [MdUpdate(*key, books_dict.get(key, None), trades_dict.get(key, None)) for key in ts] 515 | return md 516 | 517 | 518 | def load_md_from_file(path: str, nrows=10000) -> List[MdUpdate]: 519 | ''' 520 | This function downloads orderbooks ans trades and merges them 521 | ''' 522 | books = load_books(path, nrows) 523 | trades = load_trades(path, nrows) 524 | return merge_books_and_trades(books, trades) 525 | 526 | #------------------------------GETPNL--------------------------- 527 | 528 | def get_pnl(updates_list:List[ Union[MdUpdate, OwnTrade] ]) -> pd.DataFrame: 529 | ''' 530 | This function calculates PnL from list of updates 531 | ''' 532 | 533 | #current position in btc and usd 534 | btc_pos, usd_pos = 0.0, 0.0 535 | 536 | N = len(updates_list) 537 | btc_pos_arr = np.zeros((N, )) 538 | usd_pos_arr = np.zeros((N, )) 539 | mid_price_arr = np.zeros((N, )) 540 | #current best_bid and best_ask 541 | best_bid:float = -np.inf 542 | best_ask:float = np.inf 543 | 544 | for i, update in enumerate(updates_list): 545 | 546 | if isinstance(update, MdUpdate): 547 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 548 | #mid price 549 | #i use it to calculate current portfolio value 550 | mid_price = 0.5 * ( best_ask + best_bid ) 551 | 552 | if isinstance(update, OwnTrade): 553 | trade = update 554 | #update positions 555 | if trade.side == 'BID': 556 | btc_pos += trade.size 557 | usd_pos -= trade.price * trade.size 558 | elif trade.side == 'ASK': 559 | btc_pos -= trade.size 560 | usd_pos += trade.price * trade.size 561 | #current portfolio value 562 | 563 | btc_pos_arr[i] = btc_pos 564 | usd_pos_arr[i] = usd_pos 565 | mid_price_arr[i] = mid_price 566 | 567 | worth_arr = btc_pos_arr * mid_price_arr + usd_pos_arr 568 | receive_ts = [update.receive_ts for update in updates_list] 569 | exchange_ts = [update.exchange_ts for update in updates_list] 570 | 571 | df = pd.DataFrame({"exchange_ts": exchange_ts, "receive_ts":receive_ts, "total":worth_arr, "BTC":btc_pos_arr, 572 | "USD":usd_pos_arr, "mid_price":mid_price_arr}) 573 | df = df.groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 574 | return df 575 | 576 | 577 | def trade_to_dataframe(trades_list:List[OwnTrade]) -> pd.DataFrame: 578 | exchange_ts = [ trade.exchange_ts for trade in trades_list ] 579 | receive_ts = [ trade.receive_ts for trade in trades_list ] 580 | 581 | size = [ trade.size for trade in trades_list ] 582 | price = [ trade.price for trade in trades_list ] 583 | side = [trade.side for trade in trades_list ] 584 | 585 | dct = { 586 | "exchange_ts" : exchange_ts, 587 | "receive_ts" : receive_ts, 588 | "size" : size, 589 | "price" : price, 590 | "side" : side 591 | } 592 | 593 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 594 | return df 595 | 596 | 597 | def md_to_dataframe(md_list: List[MdUpdate]) -> pd.DataFrame: 598 | 599 | best_bid = -np.inf 600 | best_ask = np.inf 601 | best_bids = [] 602 | best_asks = [] 603 | for md in md_list: 604 | best_bid, best_ask = update_best_positions(best_bid, best_ask, md) 605 | best_bids.append(best_bid) 606 | best_asks.append(best_ask) 607 | 608 | exchange_ts = [ md.exchange_ts for md in md_list ] 609 | receive_ts = [ md.receive_ts for md in md_list ] 610 | dct = { 611 | "exchange_ts" : exchange_ts, 612 | "receive_ts" :receive_ts, 613 | "bid_price" : best_bids, 614 | "ask_price" : best_asks 615 | } 616 | 617 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 618 | return df 619 | 620 | if __name__ == "__main__": 621 | PATH_TO_FILE = 'D:/it/cmf/hft/week1/md/md/btcusdt_Binance_LinearPerpetual/' 622 | md = load_md_from_file(PATH_TO_FILE, nrows=2000000) 623 | latency = pd.Timedelta(10, 'ms').delta 624 | md_latency = pd.Timedelta(10, 'ms').delta 625 | sim = Sim(md, latency, md_latency) 626 | delay = pd.Timedelta(0.1, 's').delta 627 | hold_time = pd.Timedelta(10, 's').delta 628 | sigma, gamma, k = 10.0, 0.1, 1.0 629 | strategy = BestPosStrategy(delay, sigma, gamma, k, hold_time) 630 | trades_list, md_list, updates_list, all_orders, q, spread = strategy.run(sim) 631 | pnl = get_pnl(updates_list) 632 | pnl = pd.DataFrame(pnl) 633 | spread = pd.DataFrame(spread) 634 | q = pd.DataFrame(q) 635 | spread.to_csv('total_spread_stoikov.csv') 636 | q.to_csv('total_q_stoikov.csv') 637 | pnl.to_csv('total_pnl_stoikov.csv') 638 | -------------------------------------------------------------------------------- /comparison of Avellaneda-Stoikov and Gueant models/Gueant_simulator.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from collections import deque 3 | from dataclasses import dataclass 4 | #from math import gamma 5 | #from signal import Sigmasks 6 | from typing import List, Optional, Tuple, Union, Deque, Dict 7 | import numpy as np 8 | from sortedcontainers import SortedDict 9 | import math 10 | 11 | @dataclass 12 | class Order: # Our own placed order 13 | place_ts : float # ts when we place the order 14 | exchange_ts : float # ts when exchange(simulator) get the order 15 | order_id: int 16 | side: str 17 | size: float 18 | price: float 19 | 20 | 21 | @dataclass 22 | class CancelOrder: 23 | exchange_ts: float 24 | id_to_delete : int 25 | 26 | @dataclass 27 | class AnonTrade: # Market trade 28 | exchange_ts : float 29 | receive_ts : float 30 | side: str 31 | size: float 32 | price: float 33 | 34 | 35 | @dataclass 36 | class OwnTrade: # Execution of own placed order 37 | place_ts : float # ts when we call place_order method, for debugging 38 | exchange_ts: float 39 | receive_ts: float 40 | trade_id: int 41 | order_id: int 42 | side: str 43 | size: float 44 | price: float 45 | execute : str # BOOK or TRADE 46 | 47 | 48 | def __post_init__(self): 49 | assert isinstance(self.side, str) 50 | 51 | @dataclass 52 | class OrderbookSnapshotUpdate: # Orderbook tick snapshot 53 | exchange_ts : float 54 | receive_ts : float 55 | asks: List[Tuple[float, float]] # tuple[price, size] 56 | bids: List[Tuple[float, float]] 57 | 58 | 59 | @dataclass 60 | class MdUpdate: # Data of a tick 61 | exchange_ts : float 62 | receive_ts : float 63 | orderbook: Optional[OrderbookSnapshotUpdate] = None 64 | trade: Optional[AnonTrade] = None 65 | 66 | 67 | def update_best_positions(best_bid:float, best_ask:float, md:MdUpdate) -> Tuple[float, float]: 68 | if not md.orderbook is None: 69 | best_bid = md.orderbook.bids[0][0] 70 | best_ask = md.orderbook.asks[0][0] 71 | 72 | return best_bid, best_ask 73 | 74 | 75 | class Sim: 76 | def __init__(self, market_data: List[MdUpdate], execution_latency: float, md_latency: float) -> None: 77 | ''' 78 | Args: 79 | market_data(List[MdUpdate]): market data 80 | execution_latency(float): latency in nanoseconds 81 | md_latency(float): latency in nanoseconds 82 | ''' 83 | self.inventory = 0 #our current inventory 84 | 85 | #transform md to queue 86 | self.md_queue = deque( market_data ) 87 | #action queue 88 | self.actions_queue:Deque[ Union[Order, CancelOrder] ] = deque() 89 | #SordetDict: receive_ts -> [updates] 90 | self.strategy_updates_queue = SortedDict() 91 | #map : order_id -> Order 92 | self.ready_to_execute_orders:Dict[int, Order] = {} 93 | 94 | #current md 95 | self.md:Optional[MdUpdate] = None 96 | #current ids 97 | self.order_id = 0 98 | self.trade_id = 0 99 | #latency 100 | self.latency = execution_latency 101 | self.md_latency = md_latency 102 | #current bid and ask 103 | self.best_bid = -np.inf 104 | self.best_ask = np.inf 105 | self.mid_price = 0 106 | #current trade 107 | self.trade_price = {} 108 | self.trade_price['BID'] = -np.inf 109 | self.trade_price['ASK'] = np.inf 110 | #last order 111 | self.last_order:Optional[Order] = None 112 | 113 | 114 | def get_md_queue_event_time(self) -> float: 115 | return np.inf if len(self.md_queue) == 0 else self.md_queue[0].exchange_ts 116 | 117 | 118 | def get_actions_queue_event_time(self) -> float: 119 | return np.inf if len(self.actions_queue) == 0 else self.actions_queue[0].exchange_ts 120 | 121 | 122 | def get_strategy_updates_queue_event_time(self) -> float: 123 | return np.inf if len(self.strategy_updates_queue) == 0 else self.strategy_updates_queue.keys()[0] 124 | 125 | 126 | def get_order_id(self) -> int: 127 | res = self.order_id 128 | self.order_id += 1 129 | return res 130 | 131 | 132 | def get_trade_id(self) -> int: 133 | res = self.trade_id 134 | self.trade_id += 1 135 | return res 136 | 137 | 138 | def update_best_pos(self) -> None: 139 | assert not self.md is None, "no current market data!" 140 | if not self.md.orderbook is None: 141 | self.best_bid = self.md.orderbook.bids[0][0] 142 | self.best_ask = self.md.orderbook.asks[0][0] 143 | 144 | 145 | def update_last_trade(self) -> None: 146 | assert not self.md is None, "no current market data!" 147 | if not self.md.trade is None: 148 | self.trade_price[self.md.trade.side] = self.md.trade.price 149 | 150 | 151 | def delete_last_trade(self) -> None: 152 | self.trade_price['BID'] = -np.inf 153 | self.trade_price['ASK'] = np.inf 154 | 155 | 156 | def update_md(self, md:MdUpdate) -> None: 157 | #current orderbook 158 | self.md = md 159 | #update position 160 | self.update_best_pos() 161 | #update info about last trade 162 | self.update_last_trade() 163 | 164 | #add md to strategy_updates_queue 165 | if not md.receive_ts in self.strategy_updates_queue.keys(): 166 | self.strategy_updates_queue[md.receive_ts] = [] 167 | self.strategy_updates_queue[md.receive_ts].append(md) 168 | 169 | 170 | def update_action(self, action:Union[Order, CancelOrder]) -> None: 171 | 172 | if isinstance(action, Order): 173 | #self.ready_to_execute_orders[action.order_id] = action 174 | #save last order to try to execute it aggressively 175 | self.last_order = action 176 | elif isinstance(action, CancelOrder): 177 | #cancel order 178 | if action.id_to_delete in self.ready_to_execute_orders: 179 | self.ready_to_execute_orders.pop(action.id_to_delete) 180 | else: 181 | assert False, "wrong action type!" 182 | 183 | 184 | def tick(self) -> tuple([ float, float, List[ Union[OwnTrade, MdUpdate] ]]): 185 | ''' 186 | Simulation tick 187 | 188 | Returns: 189 | receive_ts(float): receive timestamp in nanoseconds 190 | res(List[Union[OwnTrade, MdUpdate]]): simulation result. 191 | ''' 192 | while True: 193 | #get event time for all the queues 194 | strategy_updates_queue_et = self.get_strategy_updates_queue_event_time() 195 | md_queue_et = self.get_md_queue_event_time() 196 | actions_queue_et = self.get_actions_queue_event_time() 197 | 198 | #if both queue are empty 199 | if md_queue_et == np.inf and actions_queue_et == np.inf: 200 | break 201 | 202 | #strategy queue has minimum event time 203 | if strategy_updates_queue_et < min(md_queue_et, actions_queue_et): 204 | break 205 | 206 | 207 | if md_queue_et <= actions_queue_et: 208 | self.update_md( self.md_queue.popleft() ) 209 | if actions_queue_et <= md_queue_et: 210 | self.update_action( self.actions_queue.popleft() ) 211 | 212 | #execute last order aggressively 213 | self.execute_last_order() 214 | #execute orders with current orderbook 215 | self.execute_orders() 216 | #delete last trade 217 | self.delete_last_trade() 218 | #end of simulation 219 | if len(self.strategy_updates_queue) == 0: 220 | return self.inventory, np.inf, None 221 | key = self.strategy_updates_queue.keys()[0] 222 | res = self.strategy_updates_queue.pop(key) 223 | 224 | #print( self.inventory, key, res) 225 | return self.inventory, key, res 226 | 227 | def execute_last_order(self) -> None: 228 | ''' 229 | this function tries to execute self.last order aggressively 230 | ''' 231 | #nothing to execute 232 | if self.last_order is None: 233 | return 234 | 235 | executed_price, execute = None, None 236 | # 237 | if self.last_order.side == 'BID' and self.last_order.price >= self.best_ask: 238 | executed_price = self.best_ask 239 | execute = 'BOOK' 240 | # 241 | elif self.last_order.side == 'ASK' and self.last_order.price <= self.best_bid: 242 | executed_price = self.best_bid 243 | execute = 'BOOK' 244 | 245 | if not executed_price is None: 246 | executed_order = OwnTrade( 247 | self.last_order.place_ts, # when we place the order 248 | self.md.exchange_ts, #exchange ts 249 | self.md.exchange_ts + self.md_latency, #receive ts 250 | self.get_trade_id(), #trade id 251 | self.last_order.order_id, 252 | self.last_order.side, 253 | self.last_order.size, 254 | executed_price, execute) 255 | #add order to strategy update queue 256 | #there is no defaultsorteddict so i have to do this 257 | if not executed_order.receive_ts in self.strategy_updates_queue: 258 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 259 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 260 | else: 261 | self.ready_to_execute_orders[self.last_order.order_id] = self.last_order 262 | 263 | #delete last order 264 | self.last_order = None 265 | 266 | 267 | def execute_orders(self) -> None: 268 | executed_orders_id = [] 269 | for order_id, order in self.ready_to_execute_orders.items(): 270 | 271 | executed_price, execute = None, None 272 | 273 | 274 | if order.side == 'BID' and order.price >= self.best_ask: 275 | executed_price = order.price 276 | execute = 'BOOK' 277 | 278 | elif order.side == 'ASK' and order.price <= self.best_bid: 279 | executed_price = order.price 280 | execute = 'BOOK' 281 | 282 | elif order.side == 'BID' and order.price >= self.trade_price['ASK']: 283 | executed_price = order.price 284 | execute = 'TRADE' 285 | 286 | elif order.side == 'ASK' and order.price <= self.trade_price['BID']: 287 | executed_price = order.price 288 | execute = 'TRADE' 289 | 290 | if not executed_price is None: 291 | executed_order = OwnTrade( 292 | order.place_ts, # when we place the order 293 | self.md.exchange_ts, #exchange ts 294 | self.md.exchange_ts + self.md_latency, #receive ts 295 | self.get_trade_id(), #trade id 296 | order_id, order.side, order.size, executed_price, execute) 297 | 298 | executed_orders_id.append(order_id) 299 | 300 | #added order to strategy update queue 301 | #there is no defaultsorteddict so i have to do this 302 | if not executed_order.receive_ts in self.strategy_updates_queue: 303 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 304 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 305 | if executed_order.side == 'BID': 306 | self.inventory += executed_order.size 307 | elif executed_order.side == 'ASK': 308 | self.inventory -= executed_order.size 309 | 310 | #deleting executed orders 311 | for k in executed_orders_id: 312 | self.ready_to_execute_orders.pop(k) 313 | 314 | 315 | def place_order(self, ts:float, size:float, side:str, price:float) -> Order: 316 | #add order into list of orders 317 | order = Order(ts, ts + self.latency, self.get_order_id(), side, size, price) 318 | self.actions_queue.append(order) 319 | return order 320 | 321 | 322 | def cancel_order(self, ts:float, id_to_delete:int) -> CancelOrder: 323 | #add order for canceling 324 | ts += self.latency 325 | delete_order = CancelOrder(ts, id_to_delete) 326 | self.actions_queue.append(delete_order) 327 | return delete_order 328 | 329 | #-------------------STRATEGY------------------------- 330 | class BestPosStrategy: 331 | ''' 332 | This strategy places ask and bid order every `delay` nanoseconds. 333 | If the order has not been executed within `hold_time` nanoseconds, it is canceled. 334 | ''' 335 | def __init__(self, df: pd.DataFrame, delay: float, sigma: float, gamma: float, k: float, A: float, hold_time:Optional[float]) -> None: 336 | ''' 337 | delay(float): delay between orders in nanoseconds 338 | sigma: volatility 339 | gamma: attitude to risk 340 | k: arrival rate 341 | A: frequency of orders in limit orderbook 342 | hold_time(Optional[float]): holding time in nanoseconds 343 | ''' 344 | self.delay = delay 345 | if hold_time is None: 346 | hold_time = max( delay * 5, pd.Timedelta(10, 's').delta ) 347 | self.hold_time = hold_time 348 | self.df = df 349 | self.sigma = sigma #volatility 350 | self.gamma = gamma #attitude to risk 351 | self.k = k #arrival rate of orders as the change in volume per second 352 | #we use it to normalize time 353 | self.A = A 354 | self.etta = A * pow(1+gamma/k, -1-k/gamma) 355 | self.T_begin = 1655942402250125991 #beginning of our session 356 | self.T_end = 1655943884469232791 #end of our session 357 | self.q = [] 358 | self.spread = [] 359 | self.vol = [] 360 | self.vols = [4.2] 361 | self.ts = set(self.df['receive_ts']) 362 | self.index = 0 363 | 364 | #------------------------------------------------------VOLATILITY---------------- 365 | def get_vol(self, receive_ts): 366 | #print(self.vols) 367 | vol = 0 368 | if receive_ts<1655955074879744572: 369 | vol = 4.20 370 | else: 371 | if receive_ts in self.ts: 372 | index = self.df[self.df['receive_ts']==receive_ts].index[0] 373 | vol = self.df.loc[index-25000:index,:]['btcusdt:Binance:LinearPerpetual_ask_price_0'].std() 374 | if vol>100: 375 | vol = 100 376 | self.vols.append(vol) 377 | else: 378 | return self.vols[-1] 379 | return vol 380 | def run(self, sim: Sim ) ->\ 381 | Tuple[ List[OwnTrade], List[MdUpdate], List[ Union[OwnTrade, MdUpdate] ], List[Order] ]: 382 | ''' 383 | This function runs simulation 384 | 385 | Args: 386 | sim(Sim): simulator 387 | Returns: 388 | trades_list(List[OwnTrade]): list of our executed trades 389 | md_list(List[MdUpdate]): list of market data received by strategy 390 | updates_list( List[ Union[OwnTrade, MdUpdate] ] ): list of all updates 391 | received by strategy(market data and information about executed trades) 392 | all_orders(List[Orted]): list of all placed orders 393 | ''' 394 | 395 | #market data list 396 | md_list:List[MdUpdate] = [] 397 | #executed trades list 398 | trades_list:List[OwnTrade] = [] 399 | #all updates list 400 | updates_list = [] 401 | #current best positions 402 | best_bid = -np.inf 403 | best_ask = np.inf 404 | 405 | #last order timestamp 406 | prev_time = -np.inf 407 | #orders that have not been executed/canceled yet 408 | ongoing_orders: Dict[int, Order] = {} 409 | all_orders = [] 410 | while True: 411 | #get update from simulator 412 | inventory, receive_ts, updates = sim.tick() 413 | self.q.append([inventory, receive_ts]) 414 | if updates is None: 415 | break 416 | #save updates 417 | updates_list += updates 418 | for update in updates: 419 | #update best position 420 | if isinstance(update, MdUpdate): 421 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 422 | md_list.append(update) 423 | elif isinstance(update, OwnTrade): 424 | trades_list.append(update) 425 | #delete executed trades from the dict 426 | if update.order_id in ongoing_orders.keys(): 427 | ongoing_orders.pop(update.order_id) 428 | else: 429 | assert False, 'invalid type of update!' 430 | 431 | if receive_ts - prev_time >= self.delay: 432 | prev_time = receive_ts 433 | #place order 434 | #calculate the mid-price 435 | mid = 0.5 * (best_bid + best_ask) 436 | #calculate volatility 437 | self.sigma = self.get_vol(receive_ts) 438 | self.vol.append([self.sigma, receive_ts]) 439 | #calculate alpha parameter (check Gueant's formula) 440 | self.alpha = self.k*self.gamma*self.sigma*0.5 441 | #calculate delta_bid and total_delta (=delta_bid+delta_ask) 442 | delta_b = np.log(1 + (self.gamma/self.k))/self.gamma + np.sqrt(self.alpha/self.etta)*(2*inventory*0.2+0.2)/(2*self.k) 443 | sum_delta = 2*np.log(1 + (self.gamma/self.k))/self.gamma + 0.2*np.sqrt(self.alpha/self.etta)/self.k 444 | #calculate current ask/bid prices 445 | bid_price = mid - delta_b 446 | ask_price = bid_price + sum_delta 447 | self.spread.append([ask_price, bid_price, receive_ts]) 448 | bid_order = sim.place_order( receive_ts, 0.001, 'BID', bid_price ) #подредачить 449 | ask_order = sim.place_order( receive_ts, 0.001, 'ASK', ask_price ) #подредачить 450 | ongoing_orders[bid_order.order_id] = bid_order 451 | ongoing_orders[ask_order.order_id] = ask_order 452 | 453 | all_orders += [bid_order, ask_order] 454 | 455 | to_cancel = [] 456 | for ID, order in ongoing_orders.items(): 457 | if order.place_ts < receive_ts - self.hold_time: 458 | sim.cancel_order( receive_ts, ID ) 459 | to_cancel.append(ID) 460 | for ID in to_cancel: 461 | ongoing_orders.pop(ID) 462 | 463 | 464 | return trades_list, md_list, updates_list, all_orders, self.q, self.spread, self.vol 465 | 466 | 467 | 468 | #--------------------------------------------------------LOADDATA---------------------------- 469 | 470 | 471 | 472 | 473 | 474 | 475 | def load_trades(path, nrows=10000) -> List[AnonTrade]: 476 | ''' 477 | This function downloads trades data 478 | 479 | Args: 480 | path(str): path to file 481 | nrows(int): number of rows to read 482 | 483 | Return: 484 | trades(List[AnonTrade]): list of trades 485 | ''' 486 | trades = pd.read_csv(path + 'trades.csv', nrows=nrows) 487 | trades = trades.loc[719000:1835700, :] 488 | #переставляю колонки, чтобы удобнее подавать их в конструктор AnonTrade 489 | trades = trades[ ['exchange_ts', 'receive_ts', 'aggro_side', 'size', 'price' ] ].sort_values(["exchange_ts", 'receive_ts']) 490 | receive_ts = trades.receive_ts.values 491 | exchange_ts = trades.exchange_ts.values 492 | trades = [ AnonTrade(*args) for args in trades.values] 493 | return trades 494 | 495 | 496 | def load_books(path, nrows=10000) -> List[OrderbookSnapshotUpdate]: 497 | ''' 498 | This function downloads orderbook market data 499 | 500 | Args: 501 | path(str): path to file 502 | nrows(int): number of rows to read 503 | 504 | Return: 505 | books(List[OrderbookSnapshotUpdate]): list of orderbooks snapshots 506 | ''' 507 | lobs = pd.read_csv(path + 'lobs.csv', nrows=nrows) 508 | lobs = lobs.loc[314000:957000, :] 509 | #lobs = lobs.iloc[:10000, :] 510 | #rename columns 511 | names = lobs.columns.values 512 | ln = len('btcusdt:Binance:LinearPerpetual_') 513 | renamer = { name:name[ln:] for name in names[2:]} 514 | renamer[' exchange_ts'] = 'exchange_ts' 515 | lobs.rename(renamer, axis=1, inplace=True) 516 | 517 | #timestamps 518 | receive_ts = lobs.receive_ts.values 519 | exchange_ts = lobs.exchange_ts.values 520 | #список ask_price, ask_vol для разных уровней стакана 521 | #размеры: len(asks) = 10, len(asks[0]) = len(lobs) 522 | asks = [list(zip(lobs[f"ask_price_{i}"],lobs[f"ask_vol_{i}"])) for i in range(10)] 523 | #транспонируем список 524 | asks = [ [asks[i][j] for i in range(len(asks))] for j in range(len(asks[0]))] 525 | #тоже самое с бидами 526 | bids = [list(zip(lobs[f"bid_price_{i}"],lobs[f"bid_vol_{i}"])) for i in range(10)] 527 | bids = [ [bids[i][j] for i in range(len(bids))] for j in range(len(bids[0]))] 528 | 529 | books = list( OrderbookSnapshotUpdate(*args) for args in zip(exchange_ts, receive_ts, asks, bids) ) 530 | return books 531 | 532 | 533 | def merge_books_and_trades(books : List[OrderbookSnapshotUpdate], trades: List[AnonTrade]) -> List[MdUpdate]: 534 | ''' 535 | This function merges lists of orderbook snapshots and trades 536 | ''' 537 | trades_dict = { (trade.exchange_ts, trade.receive_ts) : trade for trade in trades } 538 | books_dict = { (book.exchange_ts, book.receive_ts) : book for book in books } 539 | 540 | ts = sorted(trades_dict.keys() | books_dict.keys()) 541 | 542 | md = [MdUpdate(*key, books_dict.get(key, None), trades_dict.get(key, None)) for key in ts] 543 | return md 544 | 545 | 546 | def load_md_from_file(path: str, nrows=10000) -> List[MdUpdate]: 547 | ''' 548 | This function downloads orderbooks ans trades and merges them 549 | ''' 550 | books = load_books(path, nrows) 551 | trades = load_trades(path, nrows) 552 | return merge_books_and_trades(books, trades) 553 | 554 | 555 | #------------------------------GETPNL--------------------------- 556 | 557 | def get_pnl(updates_list:List[ Union[MdUpdate, OwnTrade] ]) -> pd.DataFrame: 558 | ''' 559 | This function calculates PnL from list of updates 560 | ''' 561 | 562 | #current position in btc and usd 563 | btc_pos, usd_pos = 0.0, 0.0 564 | 565 | N = len(updates_list) 566 | btc_pos_arr = np.zeros((N, )) 567 | usd_pos_arr = np.zeros((N, )) 568 | mid_price_arr = np.zeros((N, )) 569 | #current best_bid and best_ask 570 | best_bid:float = -np.inf 571 | best_ask:float = np.inf 572 | 573 | for i, update in enumerate(updates_list): 574 | 575 | if isinstance(update, MdUpdate): 576 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 577 | #mid price 578 | #i use it to calculate current portfolio value 579 | mid_price = 0.5 * ( best_ask + best_bid ) 580 | 581 | if isinstance(update, OwnTrade): 582 | trade = update 583 | #update positions 584 | if trade.side == 'BID': 585 | btc_pos += trade.size 586 | usd_pos -= trade.price * trade.size 587 | elif trade.side == 'ASK': 588 | btc_pos -= trade.size 589 | usd_pos += trade.price * trade.size 590 | #current portfolio value 591 | 592 | btc_pos_arr[i] = btc_pos 593 | usd_pos_arr[i] = usd_pos 594 | mid_price_arr[i] = mid_price 595 | 596 | worth_arr = btc_pos_arr * mid_price_arr + usd_pos_arr 597 | receive_ts = [update.receive_ts for update in updates_list] 598 | exchange_ts = [update.exchange_ts for update in updates_list] 599 | 600 | df = pd.DataFrame({"exchange_ts": exchange_ts, "receive_ts":receive_ts, "total":worth_arr, "BTC":btc_pos_arr, 601 | "USD":usd_pos_arr, "mid_price":mid_price_arr}) 602 | df = df.groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 603 | return df 604 | 605 | 606 | def trade_to_dataframe(trades_list:List[OwnTrade]) -> pd.DataFrame: 607 | exchange_ts = [ trade.exchange_ts for trade in trades_list ] 608 | receive_ts = [ trade.receive_ts for trade in trades_list ] 609 | 610 | size = [ trade.size for trade in trades_list ] 611 | price = [ trade.price for trade in trades_list ] 612 | side = [trade.side for trade in trades_list ] 613 | 614 | dct = { 615 | "exchange_ts" : exchange_ts, 616 | "receive_ts" : receive_ts, 617 | "size" : size, 618 | "price" : price, 619 | "side" : side 620 | } 621 | 622 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 623 | return df 624 | 625 | 626 | def md_to_dataframe(md_list: List[MdUpdate]) -> pd.DataFrame: 627 | 628 | best_bid = -np.inf 629 | best_ask = np.inf 630 | best_bids = [] 631 | best_asks = [] 632 | for md in md_list: 633 | best_bid, best_ask = update_best_positions(best_bid, best_ask, md) 634 | best_bids.append(best_bid) 635 | best_asks.append(best_ask) 636 | 637 | exchange_ts = [ md.exchange_ts for md in md_list ] 638 | receive_ts = [ md.receive_ts for md in md_list ] 639 | dct = { 640 | "exchange_ts" : exchange_ts, 641 | "receive_ts" :receive_ts, 642 | "bid_price" : best_bids, 643 | "ask_price" : best_asks 644 | } 645 | 646 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 647 | return df 648 | 649 | 650 | if __name__ == "__main__": 651 | PATH_TO_FILE = '../week1/md/md/btcusdt_Binance_LinearPerpetual/' 652 | df = pd.read_csv('../week1/md/md/btcusdt_Binance_LinearPerpetual/lobs.csv') 653 | start, end = 314000, 957000 654 | df = df.loc[start:end,:] 655 | md = load_md_from_file(path=PATH_TO_FILE, nrows=2000000) 656 | latency = pd.Timedelta(10, 'ms').delta 657 | md_latency = pd.Timedelta(10, 'ms').delta 658 | sim = Sim(md, latency, md_latency) 659 | delay = pd.Timedelta(0.1, 's').delta 660 | hold_time = pd.Timedelta(10, 's').delta 661 | sigma, gamma, k, A = 10.0, 0.1, 1.0, 1.0 #parameters 662 | strategy = BestPosStrategy(df, delay, 11.0, 0.001, 1.0, 1.0, hold_time) 663 | trades_list, md_list, updates_list, all_orders, q, spread, vol = strategy.run(sim) 664 | vol = pd.DataFrame(vol) 665 | vol.to_csv('moving_vol.csv') 666 | df = get_pnl(updates_list) 667 | df.to_csv('total_pnl_gueant_moving_vol.csv') 668 | spread = pd.DataFrame(spread) 669 | spread.to_csv('total_spread_gueant_moving_vol.csv') 670 | q = pd.DataFrame(q) 671 | q.to_csv('total_q_gueant_moving_vol.csv') 672 | -------------------------------------------------------------------------------- /final model/final_simulator.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from collections import deque 3 | from dataclasses import dataclass 4 | #from math import gamma 5 | #from signal import Sigmasks 6 | from typing import List, Optional, Tuple, Union, Deque, Dict 7 | import numpy as np 8 | from sortedcontainers import SortedDict 9 | import math 10 | 11 | @dataclass 12 | class Order: # Our own placed order 13 | place_ts : float # ts when we place the order 14 | exchange_ts : float # ts when exchange(simulator) get the order 15 | order_id: int 16 | side: str 17 | size: float 18 | price: float 19 | 20 | 21 | @dataclass 22 | class CancelOrder: 23 | exchange_ts: float 24 | id_to_delete : int 25 | 26 | @dataclass 27 | class AnonTrade: # Market trade 28 | exchange_ts : float 29 | receive_ts : float 30 | side: str 31 | size: float 32 | price: float 33 | 34 | 35 | @dataclass 36 | class OwnTrade: # Execution of own placed order 37 | place_ts : float # ts when we call place_order method, for debugging 38 | exchange_ts: float 39 | receive_ts: float 40 | trade_id: int 41 | order_id: int 42 | side: str 43 | size: float 44 | price: float 45 | execute : str # BOOK or TRADE 46 | 47 | 48 | def __post_init__(self): 49 | assert isinstance(self.side, str) 50 | 51 | @dataclass 52 | class OrderbookSnapshotUpdate: # Orderbook tick snapshot 53 | exchange_ts : float 54 | receive_ts : float 55 | asks: List[Tuple[float, float]] # tuple[price, size] 56 | bids: List[Tuple[float, float]] 57 | 58 | 59 | @dataclass 60 | class MdUpdate: # Data of a tick 61 | exchange_ts : float 62 | receive_ts : float 63 | orderbook: Optional[OrderbookSnapshotUpdate] = None 64 | trade: Optional[AnonTrade] = None 65 | 66 | 67 | def update_best_positions(best_bid:float, best_ask:float, md:MdUpdate) -> Tuple[float, float]: 68 | if not md.orderbook is None: 69 | best_bid = md.orderbook.bids[0][0] 70 | best_ask = md.orderbook.asks[0][0] 71 | 72 | return best_bid, best_ask 73 | 74 | 75 | class Sim: 76 | def __init__(self, market_data: List[MdUpdate], execution_latency: float, md_latency: float) -> None: 77 | ''' 78 | Args: 79 | market_data(List[MdUpdate]): market data 80 | execution_latency(float): latency in nanoseconds 81 | md_latency(float): latency in nanoseconds 82 | ''' 83 | self.inventory = 0 #our current inventory 84 | 85 | #transform md to queue 86 | self.md_queue = deque( market_data ) 87 | #action queue 88 | self.actions_queue:Deque[ Union[Order, CancelOrder] ] = deque() 89 | #SordetDict: receive_ts -> [updates] 90 | self.strategy_updates_queue = SortedDict() 91 | #map : order_id -> Order 92 | self.ready_to_execute_orders:Dict[int, Order] = {} 93 | 94 | #current md 95 | self.md:Optional[MdUpdate] = None 96 | #current ids 97 | self.order_id = 0 98 | self.trade_id = 0 99 | #latency 100 | self.latency = execution_latency 101 | self.md_latency = md_latency 102 | #current bid and ask 103 | self.best_bid = -np.inf 104 | self.best_ask = np.inf 105 | self.mid_price = 0 106 | #current trade 107 | self.trade_price = {} 108 | self.trade_price['BID'] = -np.inf 109 | self.trade_price['ASK'] = np.inf 110 | #last order 111 | self.last_order:Optional[Order] = None 112 | 113 | 114 | def get_md_queue_event_time(self) -> float: 115 | return np.inf if len(self.md_queue) == 0 else self.md_queue[0].exchange_ts 116 | 117 | 118 | def get_actions_queue_event_time(self) -> float: 119 | return np.inf if len(self.actions_queue) == 0 else self.actions_queue[0].exchange_ts 120 | 121 | 122 | def get_strategy_updates_queue_event_time(self) -> float: 123 | return np.inf if len(self.strategy_updates_queue) == 0 else self.strategy_updates_queue.keys()[0] 124 | 125 | 126 | def get_order_id(self) -> int: 127 | res = self.order_id 128 | self.order_id += 1 129 | return res 130 | 131 | 132 | def get_trade_id(self) -> int: 133 | res = self.trade_id 134 | self.trade_id += 1 135 | return res 136 | 137 | 138 | def update_best_pos(self) -> None: 139 | assert not self.md is None, "no current market data!" 140 | if not self.md.orderbook is None: 141 | self.best_bid = self.md.orderbook.bids[0][0] 142 | self.best_ask = self.md.orderbook.asks[0][0] 143 | 144 | 145 | def update_last_trade(self) -> None: 146 | assert not self.md is None, "no current market data!" 147 | if not self.md.trade is None: 148 | self.trade_price[self.md.trade.side] = self.md.trade.price 149 | 150 | 151 | def delete_last_trade(self) -> None: 152 | self.trade_price['BID'] = -np.inf 153 | self.trade_price['ASK'] = np.inf 154 | 155 | 156 | def update_md(self, md:MdUpdate) -> None: 157 | #current orderbook 158 | self.md = md 159 | #update position 160 | self.update_best_pos() 161 | #update info about last trade 162 | self.update_last_trade() 163 | 164 | #add md to strategy_updates_queue 165 | if not md.receive_ts in self.strategy_updates_queue.keys(): 166 | self.strategy_updates_queue[md.receive_ts] = [] 167 | self.strategy_updates_queue[md.receive_ts].append(md) 168 | 169 | 170 | def update_action(self, action:Union[Order, CancelOrder]) -> None: 171 | 172 | if isinstance(action, Order): 173 | #self.ready_to_execute_orders[action.order_id] = action 174 | #save last order to try to execute it aggressively 175 | self.last_order = action 176 | elif isinstance(action, CancelOrder): 177 | #cancel order 178 | if action.id_to_delete in self.ready_to_execute_orders: 179 | self.ready_to_execute_orders.pop(action.id_to_delete) 180 | else: 181 | assert False, "wrong action type!" 182 | 183 | 184 | def tick(self) -> tuple([ float, float, List[ Union[OwnTrade, MdUpdate] ]]): 185 | ''' 186 | Simulation tick 187 | Returns: 188 | receive_ts(float): receive timestamp in nanoseconds 189 | res(List[Union[OwnTrade, MdUpdate]]): simulation result. 190 | ''' 191 | while True: 192 | #get event time for all the queues 193 | strategy_updates_queue_et = self.get_strategy_updates_queue_event_time() 194 | md_queue_et = self.get_md_queue_event_time() 195 | actions_queue_et = self.get_actions_queue_event_time() 196 | 197 | #if both queue are empty 198 | if md_queue_et == np.inf and actions_queue_et == np.inf: 199 | break 200 | 201 | #strategy queue has minimum event time 202 | if strategy_updates_queue_et < min(md_queue_et, actions_queue_et): 203 | break 204 | 205 | 206 | if md_queue_et <= actions_queue_et: 207 | self.update_md( self.md_queue.popleft() ) 208 | if actions_queue_et <= md_queue_et: 209 | self.update_action( self.actions_queue.popleft() ) 210 | 211 | #execute last order aggressively 212 | self.execute_last_order() 213 | #execute orders with current orderbook 214 | self.execute_orders() 215 | #delete last trade 216 | self.delete_last_trade() 217 | #end of simulation 218 | if len(self.strategy_updates_queue) == 0: 219 | return self.inventory, np.inf, None 220 | key = self.strategy_updates_queue.keys()[0] 221 | res = self.strategy_updates_queue.pop(key) 222 | 223 | #print( self.inventory, key, res) 224 | return self.inventory, key, res 225 | 226 | def execute_last_order(self) -> None: 227 | ''' 228 | this function tries to execute self.last order aggressively 229 | ''' 230 | #nothing to execute 231 | if self.last_order is None: 232 | return 233 | 234 | executed_price, execute = None, None 235 | # 236 | if self.last_order.side == 'BID' and self.last_order.price >= self.best_ask: 237 | executed_price = self.best_ask 238 | execute = 'BOOK' 239 | # 240 | elif self.last_order.side == 'ASK' and self.last_order.price <= self.best_bid: 241 | executed_price = self.best_bid 242 | execute = 'BOOK' 243 | 244 | if not executed_price is None: 245 | executed_order = OwnTrade( 246 | self.last_order.place_ts, # when we place the order 247 | self.md.exchange_ts, #exchange ts 248 | self.md.exchange_ts + self.md_latency, #receive ts 249 | self.get_trade_id(), #trade id 250 | self.last_order.order_id, 251 | self.last_order.side, 252 | self.last_order.size, 253 | executed_price, execute) 254 | #add order to strategy update queue 255 | #there is no defaultsorteddict so i have to do this 256 | if not executed_order.receive_ts in self.strategy_updates_queue: 257 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 258 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 259 | else: 260 | self.ready_to_execute_orders[self.last_order.order_id] = self.last_order 261 | 262 | #delete last order 263 | self.last_order = None 264 | 265 | 266 | def execute_orders(self) -> None: 267 | executed_orders_id = [] 268 | for order_id, order in self.ready_to_execute_orders.items(): 269 | 270 | executed_price, execute = None, None 271 | 272 | 273 | if order.side == 'BID' and order.price >= self.best_ask: 274 | executed_price = order.price 275 | execute = 'BOOK' 276 | 277 | elif order.side == 'ASK' and order.price <= self.best_bid: 278 | executed_price = order.price 279 | execute = 'BOOK' 280 | 281 | elif order.side == 'BID' and order.price >= self.trade_price['ASK']: 282 | executed_price = order.price 283 | execute = 'TRADE' 284 | 285 | elif order.side == 'ASK' and order.price <= self.trade_price['BID']: 286 | executed_price = order.price 287 | execute = 'TRADE' 288 | 289 | if not executed_price is None: 290 | executed_order = OwnTrade( 291 | order.place_ts, # when we place the order 292 | self.md.exchange_ts, #exchange ts 293 | self.md.exchange_ts + self.md_latency, #receive ts 294 | self.get_trade_id(), #trade id 295 | order_id, order.side, order.size, executed_price, execute) 296 | 297 | executed_orders_id.append(order_id) 298 | 299 | #added order to strategy update queue 300 | #there is no defaultsorteddict so i have to do this 301 | if not executed_order.receive_ts in self.strategy_updates_queue: 302 | self.strategy_updates_queue[ executed_order.receive_ts ] = [] 303 | self.strategy_updates_queue[ executed_order.receive_ts ].append(executed_order) 304 | if executed_order.side == 'BID': 305 | self.inventory += executed_order.size 306 | elif executed_order.side == 'ASK': 307 | self.inventory -= executed_order.size 308 | 309 | #deleting executed orders 310 | for k in executed_orders_id: 311 | self.ready_to_execute_orders.pop(k) 312 | 313 | 314 | def place_order(self, ts:float, size:float, side:str, price:float) -> Order: 315 | #add order into list of orders 316 | order = Order(ts, ts + self.latency, self.get_order_id(), side, size, price) 317 | self.actions_queue.append(order) 318 | return order 319 | 320 | 321 | def cancel_order(self, ts:float, id_to_delete:int) -> CancelOrder: 322 | #add order for canceling 323 | ts += self.latency 324 | delete_order = CancelOrder(ts, id_to_delete) 325 | self.actions_queue.append(delete_order) 326 | return delete_order 327 | 328 | #-------------------STRATEGY------------------------- 329 | class BestPosStrategy: 330 | ''' 331 | This strategy places ask and bid order every `delay` nanoseconds. 332 | If the order has not been executed within `hold_time` nanoseconds, it is canceled. 333 | ''' 334 | def __init__(self, df: pd.DataFrame, delay: float, sigma: float, gamma: float, k: float, A: float, hold_time:Optional[float]) -> None: 335 | ''' 336 | delay(float): delay between orders in nanoseconds 337 | sigma: volatility 338 | gamma: attitude to risk 339 | k: arrival rate 340 | A: frequency of orders in limit orderbook 341 | hold_time(Optional[float]): holding time in nanoseconds 342 | ''' 343 | self.delay = delay 344 | if hold_time is None: 345 | hold_time = max( delay * 5, pd.Timedelta(10, 's').delta ) 346 | self.hold_time = hold_time 347 | self.df = df 348 | self.sigma = sigma #volatility 349 | self.gamma = gamma #attitude to risk 350 | self.k = k #arrival rate of orders as the change in volume per second 351 | #we use it to normalize time 352 | self.A = A 353 | self.etta = A * pow(1+gamma/k, -1-k/gamma) 354 | self.T_begin = 1672693200029407500 #beginning of our session 355 | self.T_end = 1672725599943979000 #end of our session 356 | self.q = [] 357 | self.spread = [] 358 | self.vol = [] 359 | self.vols = [1.0] 360 | self.ts = set(self.df['receive_ts']) 361 | self.index = 0 362 | 363 | #------------------------------------------------------VOLATILITY---------------- 364 | def get_vol(self, receive_ts): 365 | step = 10**4 #time interval for moving volatility 366 | if receive_ts in self.ts: 367 | index = self.df[self.df['receive_ts']==receive_ts].index[0] 368 | vol = self.df.loc[index-step:index,:]['ethusdt:Binance:Spot_ask_price_0'].std() 369 | if vol>100: 370 | vol = 100 371 | self.vols.append(vol) 372 | else: 373 | return self.vols[-1] 374 | return vol 375 | 376 | def run(self, sim: Sim ) ->\ 377 | Tuple[ List[OwnTrade], List[MdUpdate], List[ Union[OwnTrade, MdUpdate] ], List[Order] ]: 378 | ''' 379 | This function runs simulation 380 | Args: 381 | sim(Sim): simulator 382 | Returns: 383 | trades_list(List[OwnTrade]): list of our executed trades 384 | md_list(List[MdUpdate]): list of market data received by strategy 385 | updates_list( List[ Union[OwnTrade, MdUpdate] ] ): list of all updates 386 | received by strategy(market data and information about executed trades) 387 | all_orders(List[Orted]): list of all placed orders 388 | ''' 389 | 390 | #market data list 391 | md_list:List[MdUpdate] = [] 392 | #executed trades list 393 | trades_list:List[OwnTrade] = [] 394 | #all updates list 395 | updates_list = [] 396 | #current best positions 397 | best_bid = -np.inf 398 | best_ask = np.inf 399 | 400 | #last order timestamp 401 | prev_time = -np.inf 402 | #orders that have not been executed/canceled yet 403 | ongoing_orders: Dict[int, Order] = {} 404 | all_orders = [] 405 | while True: 406 | #get update from simulator 407 | inventory, receive_ts, updates = sim.tick() 408 | self.q.append([inventory, receive_ts]) 409 | if updates is None: 410 | break 411 | #save updates 412 | updates_list += updates 413 | for update in updates: 414 | #update best position 415 | if isinstance(update, MdUpdate): 416 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 417 | md_list.append(update) 418 | elif isinstance(update, OwnTrade): 419 | trades_list.append(update) 420 | #delete executed trades from the dict 421 | if update.order_id in ongoing_orders.keys(): 422 | ongoing_orders.pop(update.order_id) 423 | else: 424 | assert False, 'invalid type of update!' 425 | 426 | if receive_ts - prev_time >= self.delay: 427 | prev_time = receive_ts 428 | #place order 429 | #calculate the mid-price 430 | mid = 0.5 * (best_bid + best_ask) 431 | #calculate volatility 432 | self.sigma = self.get_vol(receive_ts) 433 | self.vol.append([self.sigma, receive_ts]) 434 | #calculate alpha parameter (check Gueant's formula) 435 | self.alpha = self.k*self.gamma*self.sigma*0.5 436 | #calculate delta_bid and total_delta (=delta_bid+delta_ask) 437 | coef1, coef2 = 0.1, 0.2 #depends on asset price 438 | delta_b = np.log(1 + (self.gamma/self.k))/self.gamma + np.sqrt(self.alpha/self.etta)*(2*inventory*coef2+coef2)/(2*self.k) 439 | sum_delta = 2*np.log(1 + (self.gamma/self.k))/self.gamma + coef2*np.sqrt(self.alpha/self.etta)/self.k 440 | #calculate current ask/bid prices 441 | 442 | bid_price = mid - coef1*delta_b 443 | ask_price = bid_price + coef1*sum_delta 444 | self.spread.append([ask_price, bid_price, receive_ts]) 445 | bid_order = sim.place_order( receive_ts, 0.001, 'BID', bid_price ) #подредачить 446 | ask_order = sim.place_order( receive_ts, 0.001, 'ASK', ask_price ) #подредачить 447 | ongoing_orders[bid_order.order_id] = bid_order 448 | ongoing_orders[ask_order.order_id] = ask_order 449 | 450 | all_orders += [bid_order, ask_order] 451 | 452 | to_cancel = [] 453 | for ID, order in ongoing_orders.items(): 454 | if order.place_ts < receive_ts - self.hold_time: 455 | sim.cancel_order( receive_ts, ID ) 456 | to_cancel.append(ID) 457 | for ID in to_cancel: 458 | ongoing_orders.pop(ID) 459 | 460 | 461 | return trades_list, md_list, updates_list, all_orders, self.q, self.spread, self.vol 462 | 463 | 464 | 465 | #--------------------------------------------------------LOADDATA---------------------------- 466 | 467 | 468 | 469 | 470 | 471 | 472 | def load_trades(path, nrows=10000) -> List[AnonTrade]: 473 | ''' 474 | This function downloads trades data 475 | Args: 476 | path(str): path to file 477 | nrows(int): number of rows to read 478 | Return: 479 | trades(List[AnonTrade]): list of trades 480 | ''' 481 | trades = pd.read_csv(path + 'trades.csv', nrows=nrows) 482 | start, end = 10**5, 2*10**5 483 | trades = trades.loc[start:end, :] #slice trades of session 484 | #переставляю колонки, чтобы удобнее подавать их в конструктор AnonTrade 485 | trades = trades[ ['exchange_ts', 'receive_ts', 'aggro_side', 'size', 'price' ] ].sort_values(["exchange_ts", 'receive_ts']) 486 | receive_ts = trades.receive_ts.values 487 | exchange_ts = trades.exchange_ts.values 488 | trades = [ AnonTrade(*args) for args in trades.values] 489 | return trades 490 | 491 | 492 | def load_books(path, nrows=10000) -> List[OrderbookSnapshotUpdate]: 493 | ''' 494 | This function downloads orderbook market data 495 | Args: 496 | path(str): path to file 497 | nrows(int): number of rows to read 498 | Return: 499 | books(List[OrderbookSnapshotUpdate]): list of orderbooks snapshots 500 | ''' 501 | lobs = pd.read_csv(path + 'lob.csv', nrows=nrows) 502 | start, end = 2*10**5, 5*10**5 #md slice 503 | lobs = lobs.loc[start:end, :] #slice lobs of session 504 | #lobs = lobs.iloc[:10000, :] 505 | #rename columns 506 | names = lobs.columns.values 507 | ln = len('ethusdt:Binance:Spot_') 508 | renamer = { name:name[ln:] for name in names[2:]} 509 | renamer[' exchange_ts'] = 'exchange_ts' 510 | lobs.rename(renamer, axis=1, inplace=True) 511 | 512 | #timestamps 513 | receive_ts = lobs.receive_ts.values 514 | exchange_ts = lobs.exchange_ts.values 515 | #список ask_price, ask_vol для разных уровней стакана 516 | #размеры: len(asks) = 10, len(asks[0]) = len(lobs) 517 | asks = [list(zip(lobs[f"ask_price_{i}"],lobs[f"ask_vol_{i}"])) for i in range(10)] 518 | #транспонируем список 519 | asks = [ [asks[i][j] for i in range(len(asks))] for j in range(len(asks[0]))] 520 | #тоже самое с бидами 521 | bids = [list(zip(lobs[f"bid_price_{i}"],lobs[f"bid_vol_{i}"])) for i in range(10)] 522 | bids = [ [bids[i][j] for i in range(len(bids))] for j in range(len(bids[0]))] 523 | 524 | books = list( OrderbookSnapshotUpdate(*args) for args in zip(exchange_ts, receive_ts, asks, bids) ) 525 | return books 526 | 527 | 528 | def merge_books_and_trades(books : List[OrderbookSnapshotUpdate], trades: List[AnonTrade]) -> List[MdUpdate]: 529 | ''' 530 | This function merges lists of orderbook snapshots and trades 531 | ''' 532 | trades_dict = { (trade.exchange_ts, trade.receive_ts) : trade for trade in trades } 533 | books_dict = { (book.exchange_ts, book.receive_ts) : book for book in books } 534 | 535 | ts = sorted(trades_dict.keys() | books_dict.keys()) 536 | 537 | md = [MdUpdate(*key, books_dict.get(key, None), trades_dict.get(key, None)) for key in ts] 538 | return md 539 | 540 | 541 | def load_md_from_file(path: str, nrows=10000) -> List[MdUpdate]: 542 | ''' 543 | This function downloads orderbooks ans trades and merges them 544 | ''' 545 | books = load_books(path, nrows) 546 | trades = load_trades(path, nrows) 547 | return merge_books_and_trades(books, trades) 548 | 549 | 550 | #------------------------------GETPNL--------------------------- 551 | 552 | def get_pnl(updates_list:List[ Union[MdUpdate, OwnTrade] ]) -> pd.DataFrame: 553 | ''' 554 | This function calculates PnL from list of updates 555 | ''' 556 | 557 | #current position in btc and usd 558 | btc_pos, usd_pos, pos = 0.0, 0.0, 0.0 559 | rebate_bid, rebate_ask = 0.99996, 1.0004 560 | 561 | N = len(updates_list) 562 | btc_pos_arr = np.zeros((N, )) 563 | posall = np.zeros((N, )) 564 | usd_pos_arr = np.zeros((N, )) 565 | mid_price_arr = np.zeros((N, )) 566 | #current best_bid and best_ask 567 | best_bid:float = -np.inf 568 | best_ask:float = np.inf 569 | 570 | for i, update in enumerate(updates_list): 571 | 572 | if isinstance(update, MdUpdate): 573 | best_bid, best_ask = update_best_positions(best_bid, best_ask, update) 574 | #mid price 575 | #i use it to calculate current portfolio value 576 | mid_price = 0.5 * ( best_ask + best_bid ) 577 | 578 | if isinstance(update, OwnTrade): 579 | trade = update 580 | #update positions 581 | if trade.side == 'BID': 582 | btc_pos += trade.size 583 | pos+=trade.size 584 | usd_pos -= trade.price * trade.size * rebate_bid 585 | elif trade.side == 'ASK': 586 | btc_pos -= trade.size 587 | pos+=trade.size 588 | usd_pos += trade.price * trade.size * rebate_ask 589 | #current portfolio value 590 | posall[i] = pos 591 | btc_pos_arr[i] = btc_pos 592 | usd_pos_arr[i] = usd_pos 593 | mid_price_arr[i] = mid_price 594 | 595 | 596 | worth_arr = btc_pos_arr * mid_price_arr + usd_pos_arr 597 | receive_ts = [update.receive_ts for update in updates_list] 598 | exchange_ts = [update.exchange_ts for update in updates_list] 599 | 600 | df = pd.DataFrame({"exchange_ts": exchange_ts, "receive_ts":receive_ts, "total":worth_arr, "BTC":posall, 601 | "USD":usd_pos_arr, "mid_price":mid_price_arr}) 602 | df = df.groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 603 | return df 604 | 605 | 606 | def trade_to_dataframe(trades_list:List[OwnTrade]) -> pd.DataFrame: 607 | exchange_ts = [ trade.exchange_ts for trade in trades_list ] 608 | receive_ts = [ trade.receive_ts for trade in trades_list ] 609 | 610 | size = [ trade.size for trade in trades_list ] 611 | price = [ trade.price for trade in trades_list ] 612 | side = [trade.side for trade in trades_list ] 613 | 614 | dct = { 615 | "exchange_ts" : exchange_ts, 616 | "receive_ts" : receive_ts, 617 | "size" : size, 618 | "price" : price, 619 | "side" : side 620 | } 621 | 622 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 623 | return df 624 | 625 | 626 | def md_to_dataframe(md_list: List[MdUpdate]) -> pd.DataFrame: 627 | 628 | best_bid = -np.inf 629 | best_ask = np.inf 630 | best_bids = [] 631 | best_asks = [] 632 | for md in md_list: 633 | best_bid, best_ask = update_best_positions(best_bid, best_ask, md) 634 | best_bids.append(best_bid) 635 | best_asks.append(best_ask) 636 | 637 | exchange_ts = [ md.exchange_ts for md in md_list ] 638 | receive_ts = [ md.receive_ts for md in md_list ] 639 | dct = { 640 | "exchange_ts" : exchange_ts, 641 | "receive_ts" :receive_ts, 642 | "bid_price" : best_bids, 643 | "ask_price" : best_asks 644 | } 645 | 646 | df = pd.DataFrame(dct).groupby('receive_ts').agg(lambda x: x.iloc[-1]).reset_index() 647 | return df 648 | 649 | 650 | if __name__ == "__main__": 651 | PATH_TO_FILE = 'ethusdt_' 652 | df = pd.read_csv('ethusdt_lob.csv') 653 | start, end = 2*10**5, 5*10**5 #md slice 654 | df = df.loc[start:end,:] 655 | md = load_md_from_file(path=PATH_TO_FILE, nrows=2000000) 656 | latency = pd.Timedelta(10, 'ms').delta 657 | md_latency = pd.Timedelta(10, 'ms').delta 658 | sim = Sim(md, latency, md_latency) 659 | delay = pd.Timedelta(0.1, 's').delta 660 | hold_time = pd.Timedelta(10, 's').delta 661 | sigma, gamma, k, A = 10.0, 1.0, 1.0, 1.0 #parameters 662 | strategy = BestPosStrategy(df, delay, sigma, gamma, k, A, hold_time) 663 | trades_list, md_list, updates_list, all_orders, q, spread, vol = strategy.run(sim) 664 | vol = pd.DataFrame(vol) 665 | vol.to_csv('vol.csv') 666 | df = get_pnl(updates_list) 667 | df.to_csv('pnl.csv') 668 | spread = pd.DataFrame(spread) 669 | spread.to_csv('spread.csv') 670 | q = pd.DataFrame(q) 671 | q.to_csv('q.csv') --------------------------------------------------------------------------------