Liquidity Measures

We have Limit Order Books of the stock GARAN for the trading days in 2017. Using bid and ask prices and volumes at different levels in the order book, we are interested in calculating the following liquidity measures for every minute interval, denoted by superscript k. Note that we are using a custom mean function defined as:

  • $mean(\vec{\textbf{x}}) := \frac { \sum_{j=1}^{dim(\vec{\textbf{x}})} x_{j} } {dim(\vec{\textbf{x}})} $

i) Average $i^{th}$ level bid depth: $\text{$\bf ABD$}^k_i$

$= mean( ( \sum^i_{j=1} \text{bsize}^k_{j,1},...,\sum^i_{j=1}\text{bsize}^k_{j,|\tau_k|}) )$

ii) Average $i^{th}$ level bid TL depth: $\text{$\bf WABD$}^k_i$

$= mean( ( \sum^i_{j=1} \text{bid}^k_{j,1} \cdot \text{bsize}^k_{j,1},...,\sum^i_{j=1} \text{bid}^k_{j,|\tau_k|} \cdot \text{bsize}^k_{j,|\tau_k|}) )$

iii)Average $i^{th}$ level ask depth: $\text{$\bf AAD$}^k_i$

$= mean( ( \sum^i_{j=1} \text{asize}^k_{j,1},...,\sum^i_{j=1}\text{asize}^k_{j,|\tau_k|}) )$

iv) Average $i^{th}$ level ask TL depth: $\text{$\bf WAAD$}^k_i$

$= mean( ( \sum^i_{j=1} \text{ask}^k_{j,1} \cdot \text{asize}^k_{j,1},...,\sum^i_{j=1} \text{ask}^k_{j,|\tau_k|} \cdot \text{asize}^k_{j,|\tau_k|}) )$

v) Average $i^{th}$ level total depth: $\text{$\bf ATD$}^k_i = \text{$\bf ABD$}^k_i + \text{$\bf AAD$}^k_i $

vi) Average $i^{th}$ level total TL depth: $\text{$\bf WATD$}^k_i = \text{$\bf WABD$}^k_i + \text{$\bf WAAD$}^k_i $

vii) Average spread: $\text{$\bf AS$}^k$

$= mean( ( \text{ask}^k_{1,1} - \text{bid}^k_{1,1},...,\text{ask}^k_{1,|\tau_k|} - \text{bid}^k_{1,|\tau_k|}) )$

viii) Average (spread/mid_price): $\text{$\bf WAS$}^k_i$

$= mean( ( \frac {\text{ask}^k_{1,1} - \text{bid}^k_{1,1}}{ \frac {\text{ask}^k_{1,1} + \text{bid}^k_{1,1}} {2}},...,\frac {\text{ask}^k_{1,|\tau_k|} - \text{bid}^k_{1,|\tau_k|}}{ \frac {\text{ask}^k_{1,|\tau_k|} + \text{bid}^k_{1,|\tau_k|}} {2}}) )$

ix) Slopes: ask side slope, bid side slope and order book slope estimated by linear regression

, where

  • $i \in \{1,2,...,\text{LOB depth}=5\}$

  • $\tau_k= \{ \text{entry times in LOB which are inside the $k^{th}$ minute interval} \} $

  • $observable^{k}_{j,l}$ : $l^{th}$ value of the observable's values inside the $k^{th}$ minute interval at level $j$

We import the Limit Order Book data frame of GARAN from Jan 2, 2017, the first trading day of the year, as an example:

In [1]:
import pandas as pd
from LiqVars import *

date=filedates_str[0]
print(date)
df = get_df(date)
df
2017-01-02
Out[1]:
bid1 bsize1 bord1 bid2 bsize2 bord2 bid3 bsize3 bord3 bid4 ... aord2 ask3 asize3 aord3 ask4 asize4 aord4 ask5 asize5 aord5
time
95508.179 7.61 110328 35 7.60 242605 15 7.59 40687 12 7.58 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
95508.323 7.60 242605 15 7.59 40687 12 7.58 97797 17 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.123 7.60 242105 15 7.59 43187 13 7.58 97797 17 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.227 7.60 132105 3 7.59 43187 13 7.58 97797 17 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.334 7.60 132105 3 7.59 48187 14 7.58 107797 19 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
180822.482 7.59 201861 26 7.58 739595 209 7.57 551466 140 7.56 ... 16 7.62 405974 53 7.63 686680 59 7.64 620202 59
180909.483 7.59 201861 26 7.58 739595 209 7.57 551466 140 7.56 ... 16 7.62 405974 53 7.63 686680 59 7.64 620202 59
180924.254 7.59 201861 26 7.58 739595 209 7.57 551466 140 7.56 ... 16 7.62 405974 53 7.63 686680 59 7.64 620202 59
180954.361 7.59 201861 26 7.58 739595 209 7.57 551466 140 7.56 ... 16 7.62 405974 53 7.63 686680 59 7.64 620202 59
180959.692 7.59 201861 26 7.58 739595 209 7.57 551466 140 7.56 ... 16 7.62 405974 53 7.63 686680 59 7.64 620202 59

7672 rows × 30 columns

Calculating the measures

We would like to calculate the liquidity measures for the LOB information contained in each minute interval. We take 10:00 AM as our starting time.

In the end, we expect a single value for each measure for every minute.

Below we have all the entries that fall into the first minute interval [10:00:00.000 , 10:01:00.000) as an example.
We have set the parameters freq=1 and precision='m', meaning we calculate the measures every minute.

In [2]:
masks, indices = get_mask_to_avg(df, freq=1, precision='m')
df.iloc[masks[0][0]:masks[0][-1]]
Out[2]:
bid1 bsize1 bord1 bid2 bsize2 bord2 bid3 bsize3 bord3 bid4 ... aord2 ask3 asize3 aord3 ask4 asize4 aord4 ask5 asize5 aord5
time
100000.123 7.6 242105 15 7.59 43187 13 7.58 97797 17 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.227 7.6 132105 3 7.59 43187 13 7.58 97797 17 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.334 7.6 132105 3 7.59 48187 14 7.58 107797 19 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.441 7.6 132105 3 7.59 48187 14 7.58 112797 20 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
100000.550 7.6 132105 3 7.59 48187 14 7.58 112797 20 7.57 ... 16 7.63 161225 9 7.64 71092 12 7.65 443326 26
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
100055.029 7.6 260744 14 7.59 304569 26 7.58 205405 29 7.57 ... 22 7.63 177426 10 7.64 236042 19 7.65 542626 29
100055.122 7.6 260744 14 7.59 304569 26 7.58 205405 29 7.57 ... 22 7.63 177426 10 7.64 236042 19 7.65 542626 29
100056.814 7.6 261744 15 7.59 304569 26 7.58 205405 29 7.57 ... 22 7.63 177426 10 7.64 236042 19 7.65 542626 29
100058.736 7.6 261744 15 7.59 304569 26 7.58 205405 29 7.57 ... 22 7.63 177426 10 7.64 236042 19 7.65 542626 29
100059.738 7.6 271744 16 7.59 304569 26 7.58 205405 29 7.57 ... 22 7.63 177426 10 7.64 236042 19 7.65 542626 29

97 rows × 30 columns

Now we calculate the measures for Jan 2, 2017. Results are shown below, rounded with a relative error tolerance of 0.1% :

In [3]:
results = get_all(df,date)
get_significant_figures(results, 2, 0.001)


Out[3]:
Stock: GARAN Date: 2017-01-02
ABD1 ABD2 ABD5 WABD1 WABD2 WABD5 AAD1 AAD2 AAD5 WAAD1 ... ATD2 ATD5 WATD1 WATD2 WATD5 AS WAS bid slope ask slope order slope
10:00 25.06•E4 5.17•E5 10.71•E5 19.05•E5 3.93•E6 8.12•E6 19.46•E4 33.84•E4 12.35•E5 1.48•E6 ... 8.55•E5 23.06•E5 33.86•E5 6.5•E6 17.55•E6 0.1•E-1 13.15•E-4 2.33•E-8 24.64•E-9 2.37•E-8
10:01 2.16•E5 5.2•E5 11.49•E5 16.42•E5 3.95•E6 8.71•E6 16.05•E4 3.52•E5 14.47•E5 12.23•E5 ... 8.72•E5 25.96•E5 28.65•E5 6.63•E6 19.77•E6 0.1•E-1 13.14•E-4 1.98•E-8 20.74•E-9 2.22•E-8
10:02 1.95•E5 4.92•E5 11.47•E5 14.83•E5 3.74•E6 8.7•E6 19.84•E4 40.06•E4 15.73•E5 1.51•E6 ... 8.92•E5 2.72•E6 29.94•E5 6.79•E6 20.72•E6 0.1•E-1 13.14•E-4 19.87•E-9 1.88•E-8 21.07•E-9
10:03 2.29•E5 5.39•E5 12.44•E5 1.74•E6 4.1•E6 9.43•E6 2.29•E5 5.43•E5 16.44•E5 17.43•E5 ... 10.82•E5 2.89•E6 3.48•E6 8.23•E6 2.2•E7 0.1•E-1 13.14•E-4 18.46•E-9 17.32•E-9 19.08•E-9
10:04 31.43•E4 5.37•E5 12.35•E5 2.39•E6 4.08•E6 9.37•E6 1.88•E5 5.28•E5 16.96•E5 1.43•E6 ... 10.64•E5 2.93•E6 3.82•E6 8.1•E6 2.23•E7 0.1•E-1 13.15•E-4 19.46•E-9 15.85•E-9 18.18•E-9
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
17:58 10.12•E5 1.94•E6 37.66•E5 7.68•E6 1.47•E7 2.85•E7 7.76•E5 1.62•E6 3.75•E6 5.9•E6 ... 3.56•E6 7.51•E6 13.58•E6 2.7•E7 5.71•E7 1.0•E-2 13.17•E-4 5.49•E-9 6.71•E-9 5.85•E-9
17:59 10.52•E5 18.95•E5 3.82•E6 7.99•E6 14.37•E6 2.89•E7 7.34•E5 15.36•E5 3.8•E6 5.58•E6 ... 3.43•E6 7.62•E6 13.56•E6 26.05•E6 5.79•E7 1.0•E-2 13.17•E-4 5.68•E-9 6.69•E-9 5.96•E-9
18:05 9.58•E5 14.34•E5 3.05•E6 7.28•E6 1.09•E7 2.31•E7 2.1•E6 2.29•E6 4.02•E6 15.97•E6 ... 3.72•E6 7.07•E6 23.25•E6 2.83•E7 5.37•E7 0.5•E-2 6.58•E-4 8.26•E-9 7.73•E-9 5.21•E-9
18:08 2.08•E5 9.47•E5 24.25•E5 15.78•E5 7.18•E6 18.36•E6 1.26•E6 14.43•E5 3.16•E6 9.57•E6 ... 2.39•E6 5.59•E6 11.15•E6 18.15•E6 4.24•E7 1.0•E-2 13.17•E-4 9.33•E-9 0.94•E-8 8.48•E-9
18:09 2.02•E5 9.41•E5 2.42•E6 15.32•E5 7.14•E6 1.83•E7 12.75•E5 14.58•E5 3.17•E6 9.69•E6 ... 2.4•E6 5.59•E6 11.22•E6 18.22•E6 4.25•E7 1.0•E-2 13.17•E-4 9.35•E-9 9.36•E-9 8.46•E-9

421 rows × 23 columns

We have one row for every minute in the LOB as we wanted.

It might occur that some minutes are not present in the LOB, which then would not appear in the results. In the case of our example the following are the minutes that never appear in the LOB:

In [4]:
print([i[:5] for i in get_clocks_inbetween('10:00:00.000',get_clocks(df)[-1][-1], freq=1, precision='m') \
                if not indices.count(i[:5])])
['13:00', '13:01', '13:02', '13:03', '13:04', '13:05', '13:06', '13:07', '13:08', '13:09', '13:10', '13:11', '13:12', '13:13', '13:14', '13:15', '13:16', '13:17', '13:18', '13:19', '13:20', '13:21', '13:22', '13:23', '13:24', '13:25', '13:26', '13:27', '13:28', '13:29', '13:30', '13:31', '13:32', '13:33', '13:34', '13:35', '13:36', '13:37', '13:38', '13:39', '13:40', '13:41', '13:42', '13:43', '13:44', '13:45', '13:46', '13:47', '13:48', '13:49', '13:50', '13:51', '13:52', '13:53', '13:54', '13:56', '13:57', '13:58', '13:59', '16:42', '16:43', '16:55', '18:00', '18:01', '18:02', '18:03', '18:04', '18:06', '18:07']

SLOPES

We start by calculating the slope of the map

$\hat{S}_{t_n}$ : Accumulated volume at $i^{th}$ level $\longrightarrow$ Average price per share
$\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\;\; X_{\pm i}= \sum_{j=1}^i x_{\pm j}\longrightarrow \hat{S}_{t_n}(X_{\pm i})= \frac { \sum_{j=1}^i p_{\pm j} x_{\pm j} } {X_{\pm i}} \;\; , \;\; x_{\pm j}$: Volume at $j^{th}$ level

for every time entry $t_n$ in the LOB, for ask side (+) and bid side (-). Here the best N ask prices are given by $p_{1}<...<p_{N}$
and the best N bid prices are given by $p_{-1}>...>p_{-N}$, which aligns with the order of levels in our LOB data frame.

Source: http://www.sciencedirect.com/science/article/pii/S0378426618301353

We take now the bid side as example and calculate $\hat{S}_{t_{3}}(X_{-i})$, where $t_{3}$ is the $3^{rd}$ row of our LOB:

In [5]:
bid_side_vols = get_vol(df,3,'bid')
bid_side_acc_vols = np.cumsum(bid_side_vols)
bid_side_prices = get_price(df,3,'bid')
print(bid_side_acc_vols,bid_side_prices)
bid_avg_price_per_share = [sum([i*k for i,k in zip(bid_side_prices[:m+1],bid_side_vols[:m+1])])/j \
                                               for m,j in enumerate(bid_side_acc_vols)]
print(bid_avg_price_per_share)
[132105. 175292. 273089. 425715. 479810.] [7.6, 7.59, 7.58, 7.57, 7.56]
[7.6, 7.597536282317505, 7.591256293735742, 7.583635554302761, 7.580970821783623]

We would like to apply linear regression on this map to get the slope but we observe a downwards slope on the bid side, whereas we would like to have an upwards one like we would get on the ask side.

In [6]:
def f(x,a,b): return a+b*x
popt_bid, _ = curve_fit(f, bid_side_acc_vols, bid_avg_price_per_share, p0=[1,1])

plt.plot(bid_side_acc_vols,bid_avg_price_per_share,'o')
plt.plot(bid_side_acc_vols,[f(i,*popt_bid) for i in bid_side_acc_vols])
print(popt_bid[-1])
-5.4738858812111624e-08

To tackle this, we could reflect the graph at y-axis:

In [7]:
bid_side_acc_vols_reflected = -bid_side_acc_vols[::-1]
bid_avg_price_per_share_reflected = bid_avg_price_per_share[::-1]

popt_bid_reflected, _ = curve_fit(f, bid_side_acc_vols_reflected, bid_avg_price_per_share_reflected, p0=[1,1])


plt.plot(bid_side_acc_vols_reflected,bid_avg_price_per_share_reflected,'o')
plt.plot(bid_side_acc_vols_reflected,[f(i,*popt_bid_reflected) for i in bid_side_acc_vols_reflected])
print(popt_bid_reflected[-1])
5.473885784597812e-08

For the limit order book slope we merge all the points from both ask and bid side and again apply linear regression:

In [8]:
ask_side_vols = get_vol(df,3,'ask')
ask_side_acc_vols = np.cumsum(ask_side_vols)
ask_side_prices = get_price(df,3,'ask')
ask_avg_price_per_share = [sum([i*k for i,k in zip(ask_side_prices[:m+1],ask_side_vols[:m+1])])/j \
                                               for m,j in enumerate(ask_side_acc_vols)]

order_acc_vols = list(bid_side_acc_vols_reflected) + list(ask_side_acc_vols)
order_avg_price_per_share = list(bid_avg_price_per_share_reflected) + list(ask_avg_price_per_share)

popt_order, _ = curve_fit(f, order_acc_vols, order_avg_price_per_share, p0=[1,1])

plt.plot(order_acc_vols,order_avg_price_per_share,'o')
plt.plot(order_acc_vols,[f(i,*popt_order) for i in order_acc_vols])
print(popt_order[-1])
3.2660719861017266e-08

Finally, all together:

In [9]:
plot_index(df,3,date,dpi=300)

Mind that these slopes are averaged over every minute as described earlier to get the desired result:

In [10]:
get_significant_figures(results[results.columns[-3:]],2,0.001)
Out[10]:
Stock: GARAN Date: 2017-01-02
bid slope ask slope order slope
10:00 2.33•E-8 24.64•E-9 2.37•E-8
10:01 1.98•E-8 20.74•E-9 2.22•E-8
10:02 19.87•E-9 1.88•E-8 21.07•E-9
10:03 18.46•E-9 17.32•E-9 19.08•E-9
10:04 19.46•E-9 15.85•E-9 18.18•E-9
... ... ... ...
17:58 5.49•E-9 6.71•E-9 5.85•E-9
17:59 5.68•E-9 6.69•E-9 5.96•E-9
18:05 8.26•E-9 7.73•E-9 5.21•E-9
18:08 9.33•E-9 0.94•E-8 8.48•E-9
18:09 9.35•E-9 9.36•E-9 8.46•E-9

421 rows × 3 columns