Why so Passive-Aggressive?
When to send limit vs market orders (i.e. passive vs aggressive execution)
Our starting point is the same as last week, namely, we hold a position q>0 in stock and want to liquidate it before time T>0. To do this, we can send either: i) limit sell orders, at price s + δ, δ >0, s=stock’s mid price, or: ii) market(able) orders, at price s - δ_0, where δ_0 is the half-spread. We’ll assume that all orders have a size q_0 that’s small (wrt the characteristic size of the model). It’ll be convenient to model the intensity of order flow by a power function
Note that, compared to our last post, since we may need to send limit orders with prices down to the mid (but no further since we assume δ>0) , we had to shift the distribution with the half-spread. Our trader is going to maximize his PnL at time T, where we now assume that there is a penalty δ_0 for any remaining inventory (in other word, in that case, the trader is forced to send a market order and pay the half-spread, ignoring all further market impact). With that in mind, the trader’s utility function u(s,x,q,t) is given as usual, as a function of mid price s, cash account x, inventory q, and time t, by
and, assuming the stock’s mid price follows a diffusion with volatility σ, u is shown to satisfy the Hamilton-Jacobi-Bellman boundary value problem
The max term expresses the choice between limit and market orders. Let’s now rewrite (3) with the help of the Ansatz
If q_0 is small enough it’s straightforward to see that h satisfies, to first order,
from which the region where h_q = 0 is easily interpreted as the stopping region, where market orders are optimal; and the region h_q > 0 is the continuation region, where limit orders are optimal.
Interestingly, (5) turns out to have a closed-form solution. To see this, let’s write down the first-order condition (δ=δ* where the sup is achieved):
so that, replacing in (5), we find the equivalent formulation
(note that F is a C^1 function). Now, in the continuation region, due to the Dirichlet boundary condition, u is simply given by the fundamental solution (in the sense of the Hopf-Lax-Oleinik formula)
where F* is the Legendre-Fenchel transform of F (with the correct sign convention):
A straightforward computation shows that
so that, applying (9), we find
Observe now that the stopping region in (7) corresponds to (ii) in (12) (since h_q=0 in ii) and h_q>0 in i)), so that no further analysis is needed: h given explicitly by (12) is the unique solution to (7).
Finally, let’s infer from (12) and (6) the optimal policy. In the continuation region, we will send limit orders at price s + δ* given by:
whereas in the stopping region:
we’ll send market orders (which will be filled at price s - δ_0 by assumption). Note that, in case δ_0 = 0, the stopping region is empty, and we recover as expected the results in last week’s post, as (13) reduces to equation (10) in that post.
Let’s summarize this in a plot:
For a given inventory, when getting too close to expiry, we will use market orders at some point, in order to be flat at t=T=1. Furthermore, the lower our inventory, and the more time we have on our hands, the more willing we are to place limit orders deep inside the book and wait in order to capture the spread, which makes intuitive sense.
Simplistic as it is, this model captures well the fundamental trade-off between executing immediately and having to pay the half spread vs waiting and hoping to collect spread. There are a number of directions where we could add more realistic features (different order sizes, market impact, drift in the stock price, other order flow intensity functions, etc).
(Python code below the fold for paying subscribers.)
Quantitatively Yours,
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
DisplayPlots = True
k = 1.2
qmax = 10000
delta0 = 0.01
c = 2 * delta0
T = 1
t0 = 0
t1 = T
nt = 300
q0 = 1
nq = 100
t_pts = np.linspace(t0,t1,nt)
q_pts = np.linspace(q0,qmax,nq)
delta = np.zeros((nt,nq))
for i in range(nt):
for j in range(nq):
tau_val = t1 - t_pts[i]
q_val = q_pts[j]
delta[i,j] = max(c * (tau_val/(q_val/qmax))**(1/k) - delta0,0)
if DisplayPlots:
t, q = np.meshgrid(t_pts, q_pts)
# Plot the contour
plt.figure()
lvls = 0.01*np.array([0.,0.00001,1.,2.,3.,4.,5.,6.])
contour_plot = plt.contourf(t, q, delta.T, cmap='coolwarm',levels=lvls,extend='max')
text_x = 0.85
text_y = 3*qmax/4
plt.text(text_x, text_y, 'MARKET\nORDERS', color='black', fontsize=15, fontweight='bold', ha='center', va='center')
text_x = 0.45
text_y = qmax/2
plt.text(text_x, text_y, 'Limit\nOrders', color='slategray', fontsize=45, fontweight='bold', ha='center', va='center')
plt.colorbar(contour_plot, label='depth ($)')
plt.title('optimal depth of order placement', fontsize=14)
plt.xlabel('time', fontsize=14)
plt.ylabel('inventory', fontsize=14)
plt.show()