Overview
Grid trading is straightforward and easy to comprehend, and it excels in high-frequency environments. However, optimizing the ideal spread, order interval, and skew can be challenging, especially as these values fluctuate over time with market conditions.
To improve grid trading’s adaptability, we can combine it with the Guéant-Lehalle-Fernandez-Tapia (GLFT) market making model.
Guéant-Lehalle-Fernandez-Tapia Market Making Model
This model represents an advanced evolution of the well-known Avellaneda-Stoikov model and provides a closed-form approximation of asymptotic behavior for terminal time T. This model does not specify a terminal time, making it suitable for typical stocks, spot assets, or crypto perpetual contracts.
We focus on equations (4.6) and (4.7) from Optimal market making :
The optimal bid quote depth, δ a p p r o x b ∗ \delta^{b*}_{approx} δ a pp ro x b ∗ , and ask quote depth, δ a p p r o x a ∗ \delta^{a*}_{approx} δ a pp ro x a ∗ , are derived from the fair price as follows:
δ a p p r o x b ∗ ( q ) = 1 ξ Δ log ( 1 + ξ Δ k ) + 2 q + Δ 2 γ σ 2 2 A Δ k ( 1 + ξ Δ k ) k ξ Δ + 1 \delta^{b*}_{approx}(q) = \frac{1}{\xi \Delta}\log\left(1 + \frac{\xi \Delta}{k}\right) + \frac{2q + \Delta}{2}\sqrt{\frac{\gamma \sigma^2}{2A\Delta k}\left(1 + \frac{\xi \Delta}{k}\right)^{\frac{k}{\xi \Delta} + 1}} δ a pp ro x b ∗ ( q ) = ξ Δ 1 log ( 1 + k ξ Δ ) + 2 2 q + Δ 2 A Δ k γ σ 2 ( 1 + k ξ Δ ) ξ Δ k + 1
δ a p p r o x a ∗ ( q ) = 1 ξ Δ log ( 1 + ξ Δ k ) − 2 q − Δ 2 γ σ 2 2 A Δ k ( 1 + ξ Δ k ) k ξ Δ + 1 \delta^{a*}_{approx}(q) = \frac{1}{\xi \Delta}\log\left(1 + \frac{\xi \Delta}{k}\right) - \frac{2q - \Delta}{2}\sqrt{\frac{\gamma \sigma^2}{2A\Delta k}\left(1 + \frac{\xi \Delta}{k}\right)^{\frac{k}{\xi \Delta} + 1}} δ a pp ro x a ∗ ( q ) = ξ Δ 1 log ( 1 + k ξ Δ ) − 2 2 q − Δ 2 A Δ k γ σ 2 ( 1 + k ξ Δ ) ξ Δ k + 1
We can introduce c 1 c_1 c 1 and c 2 c_2 c 2 and define them by extracting the volatility σ from the square root:
c 1 = 1 ξ Δ log ( 1 + ξ Δ k ) c_1 = \frac{1}{\xi \Delta}\log\left(1 + \frac{\xi \Delta}{k}\right) c 1 = ξ Δ 1 log ( 1 + k ξ Δ )
c 2 = γ 2 A Δ k ( 1 + ξ Δ k ) k ξ Δ + 1 c_2 = \sqrt{\frac{\gamma}{2A\Delta k}\left(1 + \frac{\xi \Delta}{k}\right)^{\frac{k}{\xi \Delta} + 1}} c 2 = 2 A Δ k γ ( 1 + k ξ Δ ) ξ Δ k + 1
These equations consist of the half spread and skew:
half spread = c 1 + Δ 2 σ c 2 \text{half spread} = c_1 + \frac{\Delta}{2} \sigma c_2 half spread = c 1 + 2 Δ σ c 2
skew = σ c 2 \text{skew} = \sigma c_2 skew = σ c 2
bid price = fair price − ( half spread + skew × q ) \text{bid price} = \text{fair price} - (\text{half spread} + \text{skew} \times q) bid price = fair price − ( half spread + skew × q )
ask price = fair price + ( half spread − skew × q ) \text{ask price} = \text{fair price} + (\text{half spread} - \text{skew} \times q) ask price = fair price + ( half spread − skew × q )
where q q q represents a market maker’s inventory (position).
Calculating Trading Intensity
To determine the optimal quotes, we need to compute c 1 c_1 c 1 and c 2 c_2 c 2 . This requires calibrating A A A and k k k of trading intensity and calculating the market volatility σ \sigma σ .
Trading intensity is defined as:
λ = A exp ( − k δ ) \lambda = A \exp(-k \delta) λ = A exp ( − k δ )
Measuring Trading Intensity
@njit
def measure_trading_intensity_and_volatility ( hbt ):
tick_size = hbt.depth( 0 ).tick_size
arrival_depth = np.full( 10_000_000 , np.nan, np.float64)
mid_price_chg = np.full( 10_000_000 , np.nan, np.float64)
t = 0
prev_mid_price_tick = np.nan
mid_price_tick = np.nan
# Check every 100 milliseconds
while hbt.elapse( 100_000_000 ) == 0 :
# Record market order's arrival depth from the mid-price
if not np.isnan(mid_price_tick):
depth = - np.inf
for last_trade in hbt.last_trades( 0 ):
trade_price_tick = last_trade.px / tick_size
if last_trade.ev & BUY_EVENT == BUY_EVENT :
depth = np.nanmax([trade_price_tick - mid_price_tick, depth])
else :
depth = np.nanmax([mid_price_tick - trade_price_tick, depth])
arrival_depth[t] = depth
hbt.clear_last_trades( 0 )
depth = hbt.depth( 0 )
best_bid_tick = depth.best_bid_tick
best_ask_tick = depth.best_ask_tick
prev_mid_price_tick = mid_price_tick
mid_price_tick = (best_bid_tick + best_ask_tick) / 2.0
# Record the mid-price change for volatility calculation
mid_price_chg[t] = mid_price_tick - prev_mid_price_tick
t += 1
if t >= len (arrival_depth) or t >= len (mid_price_chg):
raise Exception
return arrival_depth[:t], mid_price_chg[:t]
Calibrating A and k
Using linear regression on the logarithm of lambda:
@njit
def linear_regression ( x , y ):
sx = np.sum(x)
sy = np.sum(y)
sx2 = np.sum(x ** 2 )
sxy = np.sum(x * y)
w = len (x)
slope = (w * sxy - sx * sy) / (w * sx2 - sx ** 2 )
intercept = (sy - slope * sx) / w
return slope, intercept
# Calibrate A and k
y = np.log(lambda_)
k_, logA = linear_regression(ticks, y)
A = np.exp(logA)
k = - k_
Implementation
@njit
def compute_coeff ( xi , gamma , delta , A , k ):
inv_k = np.divide( 1 , k)
c1 = 1 / (xi * delta) * np.log( 1 + xi * delta * inv_k)
c2 = np.sqrt(np.divide(gamma, 2 * A * delta * k) * (( 1 + xi * delta * inv_k) ** (k / (xi * delta) + 1 )))
return c1, c2
@njit
def glft_market_maker ( hbt , recorder ):
tick_size = hbt.depth( 0 ).tick_size
arrival_depth = np.full( 10_000_000 , np.nan, np.float64)
mid_price_chg = np.full( 10_000_000 , np.nan, np.float64)
t = 0
A = np.nan
k = np.nan
volatility = np.nan
gamma = 0.05
delta = 1
order_qty = 1
max_position = 20
while hbt.elapse( 100_000_000 ) == 0 :
# Record market data
# ...
# Update A, k, and volatility every 5 seconds
if t % 50 == 0 :
if t >= 6_000 - 1 :
# Calibrate A, k using 10-minute window
tmp = np.zeros( 500 , np.float64)
lambda_ = measure_trading_intensity(arrival_depth[t + 1 - 6_000 :t + 1 ], tmp)
if len (lambda_) > 2 :
lambda_ = lambda_[: 70 ] / 600
x = ticks[: len (lambda_)]
y = np.log(lambda_)
k_, logA = linear_regression(x, y)
A = np.exp(logA)
k = - k_
# Update volatility
volatility = np.nanstd(mid_price_chg[t + 1 - 6_000 :t + 1 ]) * np.sqrt( 10 )
# Compute bid and ask prices
c1, c2 = compute_coeff(gamma, gamma, delta, A, k)
half_spread_tick = c1 + delta / 2 * c2 * volatility
skew = c2 * volatility
reservation_price_tick = mid_price_tick - skew * position
bid_price_tick = np.minimum(np.round(reservation_price_tick - half_spread_tick), best_bid_tick)
ask_price_tick = np.maximum(np.round(reservation_price_tick + half_spread_tick), best_ask_tick)
# Update quotes
# ...
t += 1
recorder.record(hbt)
return True
Adjustment Factors
You can introduce adjustment factors to fine-tune the calculated half spread and skew:
half spread a d j = half spread × a d j 1 \text{half spread}_{adj} = \text{half spread} \times adj_1 half spread a d j = half spread × a d j 1
skew a d j = skew × a d j 2 \text{skew}_{adj} = \text{skew} \times adj_2 skew a d j = skew × a d j 2
Example:
adj1 = 1.0 # Keep half spread as calculated
adj2 = 0.05 # Reduce skew to 5% of calculated value
half_spread_tick = (c1 + delta / 2 * c2 * volatility) * adj1
skew = c2 * volatility * adj2
Key Parameters
gamma : Risk aversion parameter (typically 0.05)
delta : Order size increment (typically 1)
A, k : Trading intensity parameters (calibrated from market data)
volatility : Market volatility (calculated from mid-price changes)
window : Calibration window size (e.g., 10 minutes = 6,000 ticks at 100ms)
The GLFT model automatically adjusts:
Spread based on market volatility
Skew based on inventory risk
Response to changing market conditions
This makes it more adaptive than fixed-parameter grid trading, especially in:
Volatile markets
Assets with changing liquidity
Environments where rebates are available
The model works best when trading intensity is accurately calibrated and when the market exhibits consistent microstructure patterns.