I am reading a paper "A Simple Way to Estimate Bid-Ask Spreads from Daily High and Low Prices" cf.A Simple Way to Estimate Bid-Ask Spreads from Daily High and Low Prices

The authors proposed the method of estimation the bid-ask spread from high and low prices of consecutive two days.

From what I can understand, there is an important assumption there that the prices follow geometric Brownian motion and, therefore, the true variance over a 2-day period is twice as large as the expectation of the variance over a single day. This property is used for the spread estimation.

Next, assume that I have more data, than just high and low prices, say, 10 min bars.

Will it improve the spread estimator if I use high and low prices of consecutive two 10 min bars instead on days? Does it contradict to the derivation for daily case?

• Jul 27, 2021 at 18:12
• It seems their approach is geared towards longer horizon studies, and for the "effective spread", as opposed to what @lehalle's answer is gearing towards, which I understand is an estimate of the "quoted spread"
– kri
Oct 3, 2021 at 21:32

If you have access to intraday data, they are better ways to estimate the bid-ask spread. If you have Open, High, Low and Close price on each 5min bin $$b$$ (or any other interval): the Close of the previous bin and the Open of this one are consecutive. Hence $$dP(b)=C(b-1)-O(b)$$ allows to define an estimate $$\psi(b)$$ of the bid-ask spread $$\psi(b):=\min_{b:\, |dP(b)|>0} |dP(b)|.$$

It is not defined on every bin of each day (sometimes $$dP(b)=0$$), but often you have several of them. You can average them to obtain an estimate for the bid-ask of the day.

Of course you can add an estimate deduced from High and Low of the bin, but it is clearly worst than this $$\phi$$.

[EDIT following Kri's comment]
There is no academic paper comparing different approaches because anyone with empirical data can compare. Moreover, under common regularity assumption:

• for the bid-ask spread: the higher frequency the better (of course it is not the same for the volatility --because of the bid-ask spread bounce at least, convicting to the well documented "signature plot" effect--),
• I can't comment, but wanted to ask about @lehalle's answer - seems intuitive and straightforward - is there any papers empirically comparing these techniques?
– kri
Sep 30, 2021 at 17:01
• @kri I thought your comment deserved an edit of my answer: this is done Oct 1, 2021 at 10:46
• Following up @lehalle's comments yet again. The estimate 𝜓(𝑏) is a point estimate over some time-interval right (i.e. 60 consecutive 1-min bars to make an hourly estimate). Then when you say "You can average them to obtain an estimate for the bid-ask of the day", do you mean to average separate such hourly estimates of 𝜓(𝑏) over some longer time span (e.g. 3-days, or 72 estimates of 𝜓(𝑏)).
– kri
Oct 3, 2021 at 21:32
• @kri you average over the day to have a bid-ask spread for the day. Of course there is in fact an intraday seasonality of the bid-ask spread, hence you can choose to average a time-weighted or trade-weighted way; results will be different. Oct 6, 2021 at 17:19

I implemented and tested the Corwin Shultz method of liquidity calculation from High and low prices as follows: Paper: https://www.scielo.br/j/bar/a/DbHB3rhpfgr8f6qRFKPSMhG/?format=pdf&lang=en

def calculate_corwin_schultz(ohlc_df): # Code written based on Corwin & Schultz (2011) for details: https://www.scielo.br/j/bar/a/DbHB3rhpfgr8f6qRFKPSMhG/?format=pdf&lang=en
# Check for null or infinite values
if ohlc_df['High'].isnull().any() or ohlc_df['Low'].isnull().any():
print("Null values detected in 'High' or 'Low'.")
return 0
if np.isinf(ohlc_df['High']).any() or np.isinf(ohlc_df['Low']).any():
print("Infinite values detected in 'High' or 'Low'.")
return 0

epsilon = 1e-10  # Small constant to prevent division by zero

beta = (np.log(ohlc_df['High'] / (ohlc_df['Low'] + epsilon)) ** 2).dropna()
beta[beta < (np.sqrt(2) / (3 - 2 * np.sqrt(2))) ** 2] = (np.sqrt(2) / (3 - 2 * np.sqrt(2))) ** 2

gamma = (np.log(ohlc_df['High'].shift(-1) / (ohlc_df['Low'].shift(-1) + epsilon)) ** 2).dropna()

alpha_arg = 2 * beta - np.sqrt(beta) / (3 - 2 * np.sqrt(2))
alpha = (np.sqrt(alpha_arg) - np.sqrt(gamma / (3 - 2 * np.sqrt(2)))).dropna()

S = (2 * (np.exp(alpha) - 1) / (1 + np.exp(alpha))).dropna()

# If standard deviation is less than 3, return mean
print("Standard Deviation:",S.std())
if S.std() < 3:
return S.mean()
# Else, return Winsorized mean
else:
return winsorize(S, limits=[0.15, 0.15]).mean()


Basically, after calculating the Spread from the high-low values as described in the paper, we return the mean spread or winsozised means spread depending on the number of outliers in the data. If there are too many outlier liquidities then we winsorize them by 15% (upper and lower) and then return the winsorized spread. This addition ensures a more representative distribution. Additionally, I added a small Elipson number in the denominator ensures that there are no divide-by-zero errors.

From the above code, you can see that if you have more granular data, such as 10-minute bars, the geometric Brownian motion assumption may not hold as strongly. Market microstructure effects, such as intraday volatility patterns, order flow imbalances, and liquidity provision, can significantly impact high and low prices at intraday frequencies. These effects could introduce bias into the Corwin and Schultz estimator if applied to high-frequency data.

Moreover, the Corwin and Schultz estimator relies on the relationship between the variance of high and low prices over two consecutive days. Using 10-minute bars would change this relationship, potentially invalidating the estimator's assumptions and affecting its accuracy.

So, instead of improving the estimator, it will introduce more noise or bias, unless the estimator is adapted and validated for high-frequency data. The original derivation for the daily case may not directly extend to intraday data. It will need some modification for intraday.