quantpylib.hft
quantpylib.hft
is our core module designed for hft-trading purposes, including modelling, analysis and simulation of high-frequency data.
quantpylib.hft.feed
is our data feeder for public information and market data such as order book and trade feeds, custom candlesticks.
quantpylib.hft.oms
is our order management system for positions, orders tracking and trade execution + management.
quantpylib.hft.lob
is our internal limit-order book representation, and is designed to store orderbook states and buffers. There are utility functions to keep accurate representations of the internal state for both book snapshots and incremental updates. A host of other utility functions related to orderbook modelling can be found.
quantpylib.hft.trades
is our internal trade buffer. There are utility functions to compute useful information and statistics from the data stored in the buffer.
quantpylib.hft.statistics
is a statistics library for hft modelling - in addition it should work seamlessly with the data structures in the quantpylib.hft.lob.LOB
and quantpylib.hft.trades.Trades
object instances.
Examples
Data Feed
Obtaining data feeds are extremely simple with quantpylib.hft.feed.Feed
objects. Simply - create a gateway object with the correct keys
and pass them in
import os
import asyncio
from pprint import pprint
from dotenv import load_dotenv
load_dotenv()
from quantpylib.gateway.master import Gateway
from quantpylib.hft.feed import Feed
keys = {
"binance": {'key':'','secret':''},
"hyperliquid": {'key':'','secret':''},
}
async def printer(data):
print(data)
async def printer_1(data):
print('1\n',data.as_dict())
async def printer_2(data):
print('2\n',data.as_dict())
async def main():
exchange,ticker = 'hyperliquid', 'BTC'
# exchange,ticker = 'binance', 'BTCUSDT'
gateway = Gateway(config_keys=keys)
await gateway.init_clients()
feed = Feed(gateway=gateway)
#code goes here
pass
if __name__ == "__main__":
asyncio.run(main())
To get a order book feed, we can write:
and we gethyperliquid/perp/l2book/BTC_depth20
. Notice how we added a handler here - this is optional. We can add arbitrary number of coroutine handlers -
when the feed is received, it will be broadcast to all handlers asynchronously. All the schema is standardized - there is no need to write exchange-dependent code.
We will get (the same message)
1
{'ts': 1725903430442,
'bids': array([[5.65460e+04, 8.75000e-03],
[5.65430e+04, 9.33710e-01],
...
[5.65220e+04, 1.98847e+00]]),
'asks': array([[5.65470e+04, 2.20767e+00],
[5.65480e+04, 1.67035e+00],
...
[5.65710e+04, 2.64770e-01]])}
2
{'ts': 1725903430442,
'bids': array([[5.65460e+04, 8.75000e-03],
[5.65430e+04, 9.33710e-01],
...
[5.65220e+04, 1.98847e+00]]),
'asks': array([[5.65470e+04, 2.20767e+00],
[5.65480e+04, 1.67035e+00],
...
[5.65710e+04, 2.64770e-01]])}
quantpylib.hft.lob.LOB
object - we can retrieve it using the feed id.
lob = feed.get_feed(l2_feed)
print(l2_feed)
print(lob) #`quantpylib.hft.lob.LOB` object
print(lob.get_mid(), lob.get_vamp(notional=30000)) #54884.5 54883.872068603436
print(lob.get_bids_buffer())
quantpylib.hft.trades.Trades
object and get the running buffer, statistics and what not:
trades_feed = await feed.add_trades_feed(exc=exchange,ticker=ticker,handler=printer)
trades = feed.get_feed(trades_feed)
await asyncio.sleep(5)
print(trades) #`quantpylib.hft.trades.Trades` object
print(trades.get_imbalance()) #0.8328041302230744
(1725904146989, 56497.0, 0.00037, -1)
(1725904147170, 56498.0, 0.01171, 1)
(1725904147773, 56498.0, 0.02284, 1)
(1725904148396, 56497.0, 0.03539, -1)
...
time, price, size, dir
. When dir = 1
, it means a taker buy initiated trade. We can also get custom candlestick bars from the streamed raw tick
data.
After add_trades_feed
has been called, we can call add_sampling_bars_feed
and
subscribe to candlestick bars constructed internally from the tick stream. Different bars are provided, including classical time-based bars as well as information bars that have sampling frequency on other variables such as tick imbalance and entropy:
import quantpylib.hft.bars as bars
information_bars = await feed.add_sampling_bars_feed(
exc=exchange,
ticker=ticker,
handler=printer,
bar_cls=bars.TickBars,
n_ticks=10
)
[1747541147009.0, 103352.0, 103353.0, 103352.0, 103352.0, 0.13585, 10, 103352.85542878174, 1747541176079.0]
[1747541177073.0, 103352.0, 103353.0, 103352.0, 103352.0, 0.12148, 10, 103352.07935462627, 1747541206012.0]
[1747541207030.0, 103352.0, 103353.0, 103352.0, 103352.0, 0.41859, 10, 103352.00229341358, 1747541221429.0]
[1747541246062.0, 103352.0, 103353.0, 103352.0, 103352.0, 0.67041, 10, 103352.25309884998, 1747541258870.0]
[1747541261102.0, 103352.0, 103355.0, 103352.0, 103355.0, 0.13013, 10, 103353.21978021978, 1747541268063.0]
[1747541328296.0, 103356.0, 103356.0, 103355.0, 103355.0, 3.42183, 10, 103355.99768544902, 1747541357001.0]
time_start,open,high,low,close,volume,n_ticks,vwap,time_end
.
Get all the running feed ids here:
['hyperliquid/perp/l2book/BTC_depth20', 'hyperliquid/perp/trades/BTC', 'hyperliquid/perp/TickBars/BTC']
Data Archival
In the previous section we have seen how easy it is to run tick data streams, add handlers and use LOB
and Trade
buffers to compute live statistics efficiently for high-frequency strategies. We would also like to have a data archival feature that allows us to store tick-data in a seamless manner to disk.
Like, one-liner seamless. Let me demonstrate. We will do the same imports and setup:
import os
import asyncio
from pprint import pprint
from dotenv import load_dotenv
load_dotenv()
from quantpylib.hft.feed import Feed
from quantpylib.gateway.master import Gateway
keys = {
"binance": {'key':'','secret':''},
"hyperliquid": {'key':'','secret':''},
}
exchanges = ['binance','hyperliquid']
async def main():
pass #<<<
if __name__ == "__main__":
asyncio.run(main())
archiver=True
and list of tickers we want to archive:
async def main():
l2_feeds = {
"binance": ['BTCUSDT','ETHUSDT'],
"hyperliquid": ['BTC','SOL']
}
trade_feeds = {
"binance": ['BTCUSDT','ETHUSDT'],
"hyperliquid": ['BTC','SOL']
}
gateway = Gateway(config_keys=keys)
await gateway.init_clients()
feed = Feed(gateway=gateway,archiver=True) #enable archiver
run_archive_scheduler
can be configured to flush data to disk and free up RAM quicker in large
scale data archival with splits
argument - by default splits=1
.
for exchange in exchanges:
l2_feed_ids = await asyncio.gather(*[
feed.add_l2_book_feed(
exc=exchange,
ticker=ticker,
depth=20,
) for ticker in l2_feeds[exchange]
])
print(l2_feed_ids)
'''
['binance/perp/l2book/BTCUSDT_depth20', 'binance/perp/l2book/ETHUSDT_depth20']
['hyperliquid/perp/l2book/BTC_depth20', 'hyperliquid/perp/l2book/SOL_depth20']
'''
trade_feed_ids = await asyncio.gather(*[
feed.add_trades_feed(
exc=exchange,
ticker=ticker,
) for ticker in trade_feeds[exchange]
])
print(trade_feed_ids)
'''
['binance/perp/trades/BTCUSDT', 'binance/perp/trades/ETHUSDT']
['hyperliquid/perp/trades/BTC', 'hyperliquid/perp/trades/SOL']
'''
await asyncio.sleep(1e59)
await feed.cleanup()
await gateway.cleanup_clients()
And you will see that at the turn of every hour, the data for that hourly period is saved like this:
so from thearchives/
folder:
./binance/perp/trades/2024/12/BTCUSDT_3014.parquet
./binance/perp/trades/2024/12/ETHUSDT_3014.parquet
./binance/perp/l2book/2024/12/BTCUSDT_depth20_3014.parquet
./binance/perp/l2book/2024/12/ETHUSDT_depth20_3014.parquet
...
./hyperliquid/perp/trades/2024/12/BTC_3014.parquet
./hyperliquid/perp/trades/2024/12/SOL_3014.parquet
./hyperliquid/perp/l2book/2024/12/BTC_depth20_3014.parquet
./hyperliquid/perp/l2book/2024/12/SOL_depth20_3014.parquet
lob = Feed.load_lob_archives(
exc='binance',
ticker='BTCUSDT',
depth=20,
start='2024-12-30:00',
end='2024-12-31:23',
)
trades = Feed.load_trade_archives(
exc='binance',
ticker='BTCUSDT',
start='2024-12-30:00',
end='2024-12-31:23',
)
archiver=True
and no scheduler -
we simply need to call
at any point in our code and the existing buffer is emptied into a parquet file with location:
For example the above call would save existing buffer to
.data_archives/hyperliquid/perp/trades/SOL.parquet
.data_archives/hyperliquid/perp/trades/BTC.parquet
.data_archives/hyperliquid/perp/l2book/BTC_depth20.parquet
.data_archives/hyperliquid/perp/l2book/SOL_depth20.parquet
#same for binance
lobs = Feed.load_lob_archive(
exc='binance',
ticker='BTCUSDT',
depth=20,
path='./data_archives/',
# raw=True
)
trades = Feed.load_trade_archive(
exc='binance',
ticker='BTCUSDT',
path='./data_archives/',
# raw=True
)
ts price size dir
0 1735569691383 92514.9 0.517 1
1 1735569691480 92514.8 0.051 -1
2 1735569691539 92514.9 0.051 1
...
OMS
It is easy to create a manager class - it is similar to thequantpylib.hft.feed.Feed
objects. Simply - create a gateway object with the correct keys and pass them in.
We will demonstrate with examples:
import os
import asyncio
from pprint import pprint
from dotenv import load_dotenv
load_dotenv()
from quantpylib.hft.oms import OMS
from quantpylib.gateway.master import Gateway
import quantpylib.standards.markets as markets
config_keys = {
'binance': {
'key': '1234',
'secret': '1234',
},
'hyperliquid': {
'key': '1234',
'secret': '1234',
}
}
gateway = Gateway(config_keys)
async def main():
await gateway.init_clients()
oms = OMS(gateway)
await oms.init()
#code goes here...
###
await oms.cleanup()
if __name__ == "__main__":
asyncio.run(main())
For all of our socket-based message handlers, we will use a generic printer to showcase results:
async def printer(data):
if isinstance(data,dict) or isinstance(data,list):
pprint(data)
else:
try: pprint(data.as_dict())
except: pprint(data.as_list())
{'base_asset': 'BTC',
'min_notional': Decimal('10.0'),
'price_precision': 1,
'quantity_precision': 5,
'quote_asset': 'USDT'}
pprint(oms.lagged_price(exc='hyperliquid', ticker='ETH')) #Decimal('2368.25')
pprint(oms.lot_precision(exc='hyperliquid', ticker='BTC')) #5
pprint(oms.price_precision(exc='hyperliquid', ticker='DOGE')) #6
pprint(oms.rounded_lots(exc='hyperliquid', ticker='BTC',amount=0.0023032)) #0.0023
pprint(oms.rounded_price(exc='hyperliquid', ticker='BTC',price=62000.1234)) #62000.1
pprint(oms.min_notional(exc='hyperliquid', ticker='BTC')) #Decimal('10.0')
pprint(oms.rand_cloid(exc='binance')) #'b486130e1b35986abc803bb79d2e675d'
pprint(oms.common_lot_precision(ex1='hyperliquid',ex2='binance',ticker1='BTC',ticker2='BTCUSDT')) #3
pprint(oms.common_price_precision(ex1='hyperliquid',ex2='binance',ticker1='BTC',ticker2='BTCUSDT')) #1
pprint(oms.common_min_notional(ex1='hyperliquid',ex2='binance',ticker1='BTC',ticker2='BTCUSDT')) #Decimal('100')
We would like to get some positions data. Note that when oms.init()
is called, all orders and positions are automatically mirrored using underlying exchange socket subscriptions. We can make connection-less request by retrieving local state:
pprint(await oms.positions_get(exc='hyperliquid')) #HTTP requests made
pprint(await oms.positions_get_all())
{'SOL': {'amount': Decimal('1.0'),
'entry': Decimal('134.81'),
'ticker': 'SOL',
'unrealized_pnl': -0.3,
'value': Decimal('134.51')}}
{'binance': {'QUANTUSDT': {'amount': Decimal('826'),
'entry': Decimal('0.1250522412206'),
'ticker': 'QUANTUSDT',
'unrealized_pnl': 1.03231002,
'value': Decimal('104.32546026')}},
'hyperliquid': {'SOL': {'amount': Decimal('1.0'),
'entry': Decimal('134.81'),
'ticker': 'SOL',
'unrealized_pnl': -0.3,
'value': Decimal('134.51')}}}
If we want to get the live positions object that tracks all positions, or register a handler on position change, we may do so. In particular, we can register handler on_update
which passes the entire positions page (and/or) on_delta
which passes the change in positions. Furthermore, the return value is the Positions
object which is 'alive', so to speak, and keeps up to date with filled orders.
live_positions = await oms.positions_mirror(exc='hyperliquid',on_update=printer,on_delta=printer)
print(live_positions)#<quantpylib.standards.portfolio.Positions object at 0x12a98c8f0>
We may do the same with orders:
for output:{1234: {
'amount': Decimal('1.0'),
'cloid': '',
'filled_sz': Decimal('0.0'),
'oid': '1234',
'ord_status': 'NEW',
'price': Decimal('100.0'),
'ticker': 'SOL',
'timestamp': 1726113126684
}
}
{'hyperliquid': {1234: {'amount': Decimal('1.0'),
'cloid': '',
'filled_sz': Decimal('0.0'),
'oid': '1234',
'ord_status': 'NEW',
'price': Decimal('100.0'),
'ticker': 'SOL',
'timestamp': 1726113126684}}}
'binance': .... {}
Orders
object:
live_orders = await oms.orders_mirror(exc='hyperliquid',on_update=printer,on_delta=printer)
print(live_orders)#<quantpylib.standards.portfolio.Orders object at 0x13252f050>
Now that we have registered some handlers for orders, and what not - let us see what the messages look like. We can make a limit order through the OMS - the parameters are the same as in Gateway
usage:
cloid = oms.rand_cloid(exc='hyperliquid')
await oms.limit_order(exc='hyperliquid',ticker='SOL',amount=1,price=129.56,cloid=cloid)
await oms.limit_order(exc='binance',ticker='SOLUSDT',price_match=markets.PRICE_MATCH_QUEUE_1,amount=1)
hyperliquid
, so let's see what gets printed:
The on_delta
handler receives two messages:
{'amount': Decimal('1'),
'cloid': '0x272e45bf06706c3259f41079a1d48d2a',
'exc': 'hyperliquid',
'filled_sz': Decimal('0'),
'last_fill_sz': Decimal('0'),
'oid': None,
'ord_status': 'PENDING',
'ord_type': None,
'price': Decimal('129.56'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154778980,
'tp': None}
{'amount': Decimal('1.0'),
'cloid': '0x272e45bf06706c3259f41079a1d48d2a',
'exc': 'hyperliquid',
'filled_sz': Decimal('0.0'),
'last_fill_sz': Decimal('0.0'),
'oid': '1234',
'ord_status': 'NEW',
'ord_type': None,
'price': Decimal('129.56'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154781307,
'tp': None}
PENDING
status. This is followed by a NEW
order which means the order was acknowledged successful by the exchange. The on_update
handler sends this order, along with all the other open orders - which we print as a list:
[{'amount': Decimal('1.0'),
...
'timestamp': 1726123317580,
'tp': None},
{'amount': Decimal('1.0'),
'cloid': '0x272e45bf06706c3259f41079a1d48d2a',
'exc': 'hyperliquid',
'filled_sz': Decimal('0.0'),
'last_fill_sz': Decimal('0.0'),
'oid': '1234',
'ord_status': 'NEW',
'ord_type': None,
'price': Decimal('129.56'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154781307,
'tp': None}]
on_delta
:
{'amount': Decimal('1.0'),
'cloid': '0x272e45bf06706c3259f41079a1d48d2a',
'exc': 'hyperliquid',
'filled_sz': Decimal('0.0'),
'last_fill_sz': Decimal('0.0'),
'oid': '1234',
'ord_status': 'CANCELLED',
'ord_type': None,
'price': Decimal('129.56'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154781307,
'tp': None}
on_update
prints the new list (not shown), this time without the cancelled order - since it is not on the orders page anymore (it is no longer open).
We can of course, do a market-order:
Which also gives aon_delta
, PENDING
message:
{'amount': Decimal('-1'),
'cloid': '0xa70c993f525b2e8106ffd60fd19af35e',
'exc': 'hyperliquid',
'filled_sz': Decimal('0'),
'last_fill_sz': Decimal('0'),
'oid': None,
'ord_status': 'PENDING',
'ord_type': None,
'price': None,
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154778980,
'tp': None}
positions
's on_delta
message is called with each fill
{'amount': Decimal('0.19'),
'delta': Decimal('-0.81'),
'entry': Decimal('134.81'),
'ticker': 'SOL'}
{'amount': Decimal('0.00'),
'delta': Decimal('-0.19'),
'entry': Decimal('134.79'),
'ticker': 'SOL'}
amount
is the new signed position held after delta
is filled - at the end of this market order our SOL
position is closed fully, so our positions
's on_update
receives the positions page (no open positions):
on the other hand the PENDING
order created is acknowledged by exchange to NEW
and then immediately FILLED
on creation with on_delta
triggers:
{'amount': Decimal('-1.0'),
'cloid': '0xa70c993f525b2e8106ffd60fd19af35e',
'exc': 'hyperliquid',
'filled_sz': Decimal('0.0'),
'last_fill_sz': Decimal('0.0'),
'oid': 'yomama',
'ord_status': 'NEW',
'ord_type': None,
'price': Decimal('127.69'), #hyperliquid's market order is an aggressive limit order
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154797325,
'tp': None}
{'amount': Decimal('-1.0'),
'cloid': '0xa70c993f525b2e8106ffd60fd19af35e',
'exc': 'hyperliquid',
'filled_sz': Decimal('1.0'),
'last_fill_sz': Decimal('1.0'),
'oid': 'yomama',
'ord_status': 'FILLED',
'ord_type': None,
'price': Decimal('127.69'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726154797325,
'tp': None}
on_update
triggers:
We will demonstrate a complex order supported by the OMS - let's call it hedge_order
. It is quite often that we want one order to trigger another in a multi-leg trade. For instance, a triangular arbitrage, cross-exchange market making, funding arbitrage and l/s arbitrage all often use similar fixtures. A hedge order allows us to submit a maker-order, and the matching taker order is triggered with size matching that of the filled amount on the maker-leg. When lot size rounding doesn't allow for complete hedging, the remaining balance is stored and flushed with the next best available order. Let's see how we can make use of this. To get information from both exchanges, we wil add the binance handlers:
await oms.positions_mirror(exc='binance',on_update=printer,on_delta=printer)
await oms.orders_mirror(exc='binance',on_update=printer,on_delta=printer)
await asyncio.sleep(5)
await oms.hedge_order(
maker_order = {
"exc": "binance",
"ticker": "SOLUSDT",
"amount": -3,
"price_match": markets.PRICE_MATCH_QUEUE_5
},
hedge_order = {
"exc": "hyperliquid",
"ticker": "SOL",
}
)
>> orders delta:
{'amount': Decimal('-3'),
'cloid': 'c484811d8ce145004eeb26c917013c29',
'exc': 'binance',
'filled_sz': Decimal('0'),
'last_fill_sz': Decimal('0'),
'oid': None,
'ord_status': 'PENDING',
'ord_type': None,
'price': None,
'price_match': 'QUEUE_5',
'reduce_only': None,
'sl': None,
'ticker': 'SOLUSDT',
'tif': None,
'timestamp': 1726199235551,
'tp': None}
{'amount': Decimal('-3'),
'cloid': 'c484811d8ce145004eeb26c917013c29',
'exc': 'binance',
'filled_sz': Decimal('0'),
'last_fill_sz': Decimal('0'),
'oid': '68254554715',
'ord_status': 'NEW',
'ord_type': 'LIMIT',
'price': Decimal('134.5460'),
'price_match': 'QUEUE_5',
'reduce_only': False,
'sl': None,
'ticker': 'SOLUSDT',
'tif': 'GTC',
'timestamp': 1726199241966,
'tp': None}
>> orders snapshot:
[{'amount': Decimal('-3'),
'cloid': 'c484811d8ce145004eeb26c917013c29',
'exc': 'binance',
'filled_sz': Decimal('0'),
'last_fill_sz': Decimal('0'),
'oid': '68254554715',
'ord_status': 'NEW',
'ord_type': 'LIMIT',
'price': Decimal('134.5460'),
'price_match': 'QUEUE_5',
'reduce_only': False,
'sl': None,
'ticker': 'SOLUSDT',
'tif': 'GTC',
'timestamp': 1726199241966,
'tp': None}]
>> positions delta:
{'amount': Decimal('-3'),
'delta': Decimal('-3'),
'entry': Decimal('134.546'),
'ticker': 'SOLUSDT'}
>> positions snapshot:
{'SOLUSDT': {'amount': Decimal('-3'),
'entry': Decimal('134.546'),
'ticker': 'SOLUSDT'}}
{'amount': Decimal('-3'),
'cloid': 'c484811d8ce145004eeb26c917013c29',
'exc': 'binance',
'filled_sz': Decimal('3'),
'last_fill_sz': Decimal('3'),
'oid': '68254554715',
'ord_status': 'FILLED',
'ord_type': 'LIMIT',
'price': Decimal('134.5460'),
'price_match': 'QUEUE_5',
'reduce_only': False,
'sl': None,
'ticker': 'SOLUSDT',
'tif': 'GTC',
'timestamp': 1726199256915,
'tp': None}
PENDING
to NEW
to FILLED
{'amount': Decimal('3.0'),
'cloid': '0xb09f965b3b58613bce2b12f15a94ad47',
'exc': 'hyperliquid',
'filled_sz': Decimal('0'),
'last_fill_sz': Decimal('0'),
'oid': None,
'ord_status': 'PENDING',
'ord_type': None,
'price': None,
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726199235551,
'tp': None}
{'amount': Decimal('3.0'),
'cloid': '0xb09f965b3b58613bce2b12f15a94ad47',
'exc': 'hyperliquid',
'filled_sz': Decimal('0.0'),
'last_fill_sz': Decimal('0.0'),
'oid': '37771235449',
'ord_status': 'NEW',
'ord_type': None,
'price': Decimal('141.36'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726199257706,
'tp': None}
{'amount': Decimal('3.0'),
'cloid': '0xb09f965b3b58613bce2b12f15a94ad47',
'exc': 'hyperliquid',
'filled_sz': Decimal('3.0'),
'last_fill_sz': Decimal('3.0'),
'oid': '37771235449',
'ord_status': 'FILLED',
'ord_type': None,
'price': Decimal('141.36'),
'price_match': None,
'reduce_only': None,
'sl': None,
'ticker': 'SOL',
'tif': None,
'timestamp': 1726199257706,
'tp': None}
{'amount': Decimal('3.0'),
'delta': Decimal('3.0'),
'entry': Decimal('134.65'),
'ticker': 'SOL'}
{'SOL': {'amount': Decimal('3.0'), 'entry': Decimal('134.65'), 'ticker': 'SOL'}}
Market-Making
Please refer to the market-making tutorial using the Feed
and OMS
object to write cross-exchange, cross environment (live, backtest and modelling) code for high-frequency data.
Limit-Order Book
You may want to use the orderbook with your own data stream. Here is an example of how you can do it (speudo-code)
from quantpylib.hft.lob import LOB
ob = LOB(depth=depth,buffer_size=buffer_size)
async def on_stream(msg):
ob.update(msg['ts'],msg['b'],msg['a'],is_snapshot=True / False)
A limit order book object is only interesting with data already in it. Although we can create a LOB
object directly, for demonstration - we are going to obtain it from a market data stream. Fortunately, our quantpylib.gateway.executor
library has a l2_book_mirror
method that returns a live orderbook object.
We will get data from hyperliquid through the gateway object:
import asyncio
from quantpylib.gateway.master import Gateway
gateway = Gateway(config_keys={"hyperliquid":{}})
async def ob_handler(ob):
print(ob.get_mid(), ob.get_spread())
return
async def main():
await gateway.init_clients()
ob_model = await gateway.executor.l2_book_mirror(
ticker="SOL",
depth=20,
buffer_size=200,
as_dict=False,
on_update=ob_handler,
exc='hyperliquid'
)
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(main())
l2_book_mirror
method in quantpylib.wrappers.hyperliquid
wrapper, and as_dict=False
specifices we want a quantpylib.hft.lob.LOB
object.
We can now use the utility functions of the orderbook class. While we are sleeping, the ob_handler
prints out...
182.04500000000002 0.010000000000019327
182.04500000000002 0.010000000000019327
182.04500000000002 0.010000000000019327
182.05 0.020000000000010232
182.075 0.010000000000019327
182.075 0.010000000000019327
182.075 0.010000000000019327
182.075 0.010000000000019327
LOB.update
method here, since the mirror method takes care of the state of the orderbook on data stream. Let's take a look at some of the utility functions after sleeping:
print(ob_model.get_mid(),ob_model.get_vamp(notional=3000))
print(ob_model.get_vol(exp=True))
print(ob_model.buffer_len())
Trades
Like the LOB
object, Trades
is a trade buffer stream. Using the data streamed in, we may get useful information such as trade imbalance.
Filling in the trade buffer is extremely easy, let's get the hyperliquid BTC trade stream:
import asyncio
from quantpylib.hft.trades import Trades
from quantpylib.wrappers.hyperliquid import Hyperliquid
async def main():
ticker = "BTC"
hpl = Hyperliquid()
await hpl.init_client()
trades = Trades(buffer_size=1_000_000)
async def trade_handler(trade):
#trade is (time_ms, price, size, dir)
#>>> (1722177301662, 67749.0, 0.0003, -1)
trades.append(trade=trade)
await hpl.trades_subscribe(ticker=ticker,handler=trade_handler)
await asyncio.sleep(1000)
Statistics
This is our statistical library for hft modelling. It is designed to work seamlessly with the data structures from our internal state representations, such as the orderbook LOB
and trades Trades
, but will work just as well with external data.
For instance, we may be interested in computing orderbook liquidity - to do this we fit an exponential decay model for the hit and -lifted amounts against the distance to mid price. This figure is directly related to the Poisson intensity that is often taken as a model for trade arrivals.
Let's stream both the l2-book data and the trades occuring:
import asyncio
import numpy as np
import matplotlib.pyplot as plt
from quantpylib.hft.lob import LOB
from quantpylib.hft.trades import Trades
from quantpylib.hft.stats import intensity
from quantpylib.wrappers.hyperliquid import Hyperliquid
async def main():
ticker = "BTC"
hpl = Hyperliquid()
await hpl.init_client()
ob = await hpl.l2_book_mirror(
ticker=ticker,
depth=20,
buffer_size=1_000_000,
as_dict=False
)
trades = Trades(buffer_size=1_000_000)
async def trade_handler(trade):
trades.append(trade=trade)
await hpl.trades_subscribe(ticker=ticker,handler=trade_handler)
await asyncio.sleep(60 * 20)
#code goes here...
if __name__ == "__main__":
asyncio.run(main())
quantpylib.hft.stats.intensity
, and make some plots:
print(ob.buffer_len()) #2051
print(trades.buffer_len()) #408
params = intensity(
lob_timestamps=ob.get_ts_buffer(),
lob_mids=ob.get_mids_buffer(),
trades=trades.get_buffer(),
)
alpha = params.get("alpha")
kappa = params.get("kappa")
levels = params.get("levels")
agg_amounts = params.get("amounts")
fitted_values = alpha * np.exp(-kappa * levels)
# Plot the actual and fitted
plt.figure(figsize=(10, 6))
plt.plot(levels, agg_amounts, 'o', label='Aggregated Amounts', color='red')
plt.plot(levels, fitted_values, '-', label=f'Fitted Model: $A(d) = {alpha:.2f} e^{{-{kappa:.2f} d}}$', color='blue')
plt.xlabel('Distance from Mid-Price (d)')
plt.ylabel('Aggregated Trade Amount (A(d))')
plt.title('Exponential Decay Model Fit to Aggregated Trade Amounts')
plt.legend()
plt.grid(True)
plt.show()
Values such as kappa are often used as measures of orderbook liquidity. Higher values of kappa indicate strong decay, hence greater trading near the mid-price. Lower values indicate weak decay - market order sizes often wipe out a few levels in the orderbook and have strong price impact. See Hummingbot implementation of computing trade intensity. In stoikov-avellaneda market making formula, kappa appears as term in optimal spread; see Hummingbot avellaneda_market_making.pyx:
self._optimal_spread = self.gamma * vol * time_left_fraction
self._optimal_spread += 2 * Decimal(1 + self.gamma / self._kappa).ln() / self.gamma