This notebook presents a model for pricing options in a market with proportional transaction costs. The model is taken from the celebrated work of Davis-Panas-Zariphopoulou 1993 link-to-the-paper.
However, due to its complexity (and time complexity), it is not very well known to the practitioners.
The purpose of this notebook is to explain in simple terms the main ideas of the model, and show how to implement it numerically. The results will surprise you!
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from IPython.display import display
%matplotlib inline
Let us consider a portfolio composed by:
The 3-D state of the portfolio at time t∈[t0,T] is (Bt,Yt,St) and evolves following the SDE:
{dBt=rBtdt−(1+θb)StdLt+(1−θs)StdMtdYt=dLt−dMtdSt=St(μdt+σdWt).The parameters θb, θs≥0 are the proportional transaction costs when buying and selling respectively.
The processes {(Lt,Mt)}t∈[t0,T] are the trading strategies, i.e. the controls of the problem.
The process {Lt}t represents the cumulative number of shares bought up to time t, and {Mt}t represents the number of shares sold up to time t. They are right-continuous, finite variation, non-negative and increasing processes. If the time t is a discontinuous point (there is a transaction), the variation of the processes are indicated as ΔLt=L(t)−L(t−)ΔMt=M(t)−M(t−)
Let us consider an example. If at time t, the investor wants to buy ΔLt shares. Then the portfolio changes as
ΔYt=ΔLt⏟shares boughtΔBt=−(1+θb)St⏟adjusted priceΔLtwhere the adjusted price is the real cost of the stock (incorporating the transaction cost).
If there are no transactions, the portfolio has the simple well known evolution:
{dBt=rBtdtdYt=0dSt=St(μdt+σdWt).The cash value function c(y,s):R×R+→R, is defined as the value in cash when the shares in the portfolio are liquidated i.e.
long positions are sold and short positions are covered.
For t∈[t0,T], the total wealth process in a portfolio with zero options is defined as:
W0t:=Bt+c(Yt,St).If the portfolio contains an option with maturity T and strike K, then the wealth process becomes:
Writer:
Buyer:
Wbt=Bt+c(Yt,St)1{t<T}+c(Yt,St)1{t=T,St(1+θb)≤K}+(c(Yt+1,St)−K)1{t=T,St(1+θb)>K}
For t0≤t<T, the wealths Wwt and Wbt are equal to W0t, but when t=T they differ because of the payoff of the option. The writer gives away a share and recives the strike and the buyer receive a share and pays the strike.
Note that considering a market with transaction costs, implies a different condition for the exercise of the option. Now the buyer should exercise if St(1+θb)>K, because the true price of the share incorporates the value of the transaction costs. Let's see the plot:
S = np.linspace(14, 16, 100)
K = 15 # strike
cost_b = 0.01 # transaction cost
plt.plot(S, np.maximum(S - K, 0), color="blue", label="Zero cost Payoff")
plt.plot(S, np.maximum(S * (1 + cost_b) - K, 0), color="red", label="Transaction costs Payoff")
plt.xlabel("S")
plt.ylabel("Payoff")
plt.title("Payoff comparison")
plt.legend(loc="upper left")
plt.show()
The value function of the maximization problem for j=0,w,b (corresponding to the three portfolios: no option, writer, buyer) is defined as:
Vj(t,b,y,s)=supL,ME[U(WjT)|Bt=b,Yt=y,St=s],where U:R→R is a concave increasing utility function. The exponential utility is what we are looking for:
U(x):=1−e−γxγ>0.The writer (buyer) option price is defined as the amount of cash to add (subtract) to the bank account, such that the maximal expected utility of wealth of the writer (buyer) is the same he could get with the zero-option portfolio.
V0(t,b,y,s)=Vw(t,b+pw,y,s),
V0(t,b,y,s)=Vb(t,b−pb,y,s).
Using the properties of the exponential utility, it is possible to remove B from the state variables.
Vj(t,b,y,s)=supL,MEt,b,y,s[1−e−γWj(T)]=1−e−γbδ(t,T)Qj(t,y,s),where δ(t,T)=e−r(T−t). (for the full calculations, check the paper. Equations 4.21 -4.25).
The exponential term inside the expectation can be considered as a discount factor, and the second term is the terminal payoff:
H0(y,s)=e−γc(y,s).
Hw(y,s)=e−γ[c(y,s)1{s(1+θb)≤K}+(c(y−1,s)+K)1{s(1+θb)>K}].
In order to simplify the numerical computations,let us pass to the log-variable Xt=log(St).
The resulting portfolio dynamics is:
{dYt=dLt−dMtdXt=(μ−12σ2)dt+σdWt.The Hamilton Jacobi Bellman equation associated to the minimization problem is:
min{∂Qj∂t+(μ−12σ2)∂Qj∂x+12σ2∂2Qj∂x2,∂Qj∂y+(1+θb)exγδ(t,T)Qj,−(∂Qj∂y+(1−θs)exγδ(t,T)Qj)}=0.Using again the explicit form of the utility function, we obtain formulas for the option prices:
pw(t0,y,x)=δ(t0,T)γlog(Qw(t0,y,ex)Q0(t0,y,ex)),As usual, we introduced the time step Δt=TN, where we assumed t0=0 and N∈N. The time tn=nΔt, for n∈{0,1,2,...,N}.
The space discretization has 2 dimensions:
The discretized version of the Stochastic Differential equation is:
{ΔYn=ΔLn−ΔMnΔXn=(μ−12σ2)Δt+σΔWnBoth ΔLn and ΔMn at each time tn can assume values in {0,hy}. They cannot be different from zero at the same time (It is quite strange to buy and sell at the same time, right?)
The variable ΔWn has 12 probability of being equal to hx and 12 probability of being equal to −hx.
The variation ΔXn is ±hx plus the drift component. We obtain a recombining binomial tree.
N = 6
dt = 1 / N
S0 = 15
x0 = np.log(S0)
mu = 0.1
sig = 0.25
h_x = sig * np.sqrt(dt)
for n in range(N):
x = np.array([x0 + (mu - 0.5 * sig**2) * dt * n + (2 * i - n) * h_x for i in range(n + 1)])
print(x)
[2.7080502] [2.61744646 2.82157061] [2.52684272 2.73096687 2.93509101] [2.43623898 2.64036313 2.84448727 3.04861142] [2.34563524 2.54975939 2.75388353 2.95800768 3.16213182] [2.2550315 2.45915565 2.6632798 2.86740394 3.07152809 3.27565223]
Using the Dynamic Programming Principle (DPP) on the minimization problem we obtain a recursive algorithm on the nodes of the grid.
Qj(tn,Yn,Xn)=min{En[Q(tn+1,Yn,Xn+ΔXn)],exp(γδ(tn,T)(1+θb)eXnΔLn)En[Qj(tn+1,Yn+ΔLn,Xn+ΔXn)],exp(−γδ(tn,T)(1−θs)eXnΔMn)En[Qj(tn+1,Yn−ΔMn,Xn+ΔXn)]}.The loop over all the nodes is O(N2).
Therefore the total computational complexity is of O(N4).
Let us import the classes we need:
from FMNM.Parameters import Option_param
from FMNM.Processes import Diffusion_process
from FMNM.TC_pricer import TC_pricer
from FMNM.BS_pricer import BS_pricer
# Creates the object with the parameters of the option
opt_param = Option_param(S0=15, K=15, T=1, exercise="European", payoff="call")
# Creates the object with the parameters of the process
diff_param = Diffusion_process(r=0.1, sig=0.25, mu=0.1)
# Creates the object of the Transaction Costs pricer
TC = TC_pricer(opt_param, diff_param, cost_b=0, cost_s=0, gamma=0.0001)
# Creates the object of the Black-Scholes pricer
BS = BS_pricer(opt_param, diff_param)
We expect that if the transaction costs are zero, and the risk aversion coefficient γ→0 (i.e. the investor becomes risk neutral), the price should converge to the Black-Scholes price
tc = TC.price(N=2000)
bs = BS.closed_formula()
print("Zero TC price: ", tc)
print("Black Scholes price:", bs)
print("Difference:", np.abs(tc - bs))
Zero TC price: 2.246375063664713 Black Scholes price: 2.246368616746695 Difference: 6.446918018099268e-06
S = list(range(5, 21))
price_0 = []
price_w = []
price_b = []
for s in S:
TC.S0 = s
TC.cost_b = 0
TC.cost_s = 0
price_0.append(TC.price(N=400)) # zero costs
TC.cost_b = 0.05
TC.cost_s = 0.05
price_w.append(TC.price(N=400, TYPE="writer"))
price_b.append(TC.price(N=400, TYPE="buyer"))
TC.cost_b = 0
TC.cost_s = 0 # set to 0 for future computations
plt.plot(S, price_0, color="blue", marker="s", label="Zero costs")
plt.plot(S, price_w, color="green", marker="o", label="Writer")
plt.plot(S, price_b, color="magenta", marker="*", label="Buyer")
BS.plot(axis=[10, 20, 0, 8]) # plot of the Black Scholes line
If we set the "Time" argument to True, the method also returns the execution time. Let us verify that the algorithm has time complexity of order O(N4)
The following operation will be very time consuming. In case you are in a hurry, reduce the NUM.
NUM = 7
price_table = pd.DataFrame(columns=["N", "Price", "Time"])
for j, n in enumerate([50 * 2**i for i in range(NUM)]):
price_table.loc[j] = [n] + list(TC.price(n, Time=True))
display(price_table)
N | Price | Time | |
---|---|---|---|
0 | 50.0 | 6.531228 | 0.008018 |
1 | 100.0 | 6.533475 | 0.029627 |
2 | 200.0 | 6.533735 | 0.136240 |
3 | 400.0 | 6.534301 | 0.613247 |
4 | 800.0 | 6.534227 | 6.132332 |
5 | 1600.0 | 6.534322 | 44.816548 |
6 | 3200.0 | 6.534285 | 407.323948 |
Using the computational times we can estimate the exponent α of the polinomial growth O(Nα).
For higher values of N, the exponent converges to the expected value of α=4.
Here we are quite close.
print("The exponent is: ", np.log2(price_table["Time"][6] / price_table["Time"][5]))
The exponent is: 3.1840732004452104
The coefficient γ measure the risk aversion of the investor. We can see how the option price is affected by this coefficient:
price_ww = []
price_bb = []
gammas = [0.0001, 0.001, 0.01, 0.05, 0.1, 0.3, 0.5]
TC.cost_b = 0.01
TC.cost_s = 0.01
for gamma in gammas:
TC.gamma = gamma
price_ww.append(TC.price(N=400, TYPE="writer"))
price_bb.append(TC.price(N=400, TYPE="buyer"))
plt.plot(gammas, price_ww, color="green", marker="o", label="Writer")
plt.plot(gammas, price_bb, color="magenta", marker="*", label="Buyer")
plt.xlabel("gamma")
plt.ylabel("price")
plt.legend()
plt.show()
So far we have found that:
The option pricing is an increasing function of the risk aversion coefficient for the writer, and a decreasing function for the buyer.
The option pricing is an increasing function of the transaction costs for the writer, and a decreasing function for the buyer.
As we know from the "classical" no-arbitrage martingale pricing theory, the option price does not depend on the stock expected value.
However, this model is a utility based model i.e. a model that does not consider a risk neutral investor.
We can see that in this model the option price depends on the drift.
If we consider a high risk aversion coefficient, the option price is not very sensitive to the drift. If instead we choose a small value of γ, i.e. the investor is risk neutral, the drift plays the role of the risk neutral expected return r and therefore changing μ, is like changing r.
Following Hodges-Neuberger [2], in the practical computations it is better to set μ=r.
price_mu1 = []
price_mu2 = []
mus = [0.01, 0.05, 0.1, 0.2, 0.3]
TC.gamma = 1 # high value of risk aversion
TC.cost_b = 0.01
TC.cost_s = 0.01
for mu in mus:
TC.mu = mu
price_mu1.append(TC.price(N=400, TYPE="writer"))
price_mu2.append(TC.price(N=400, TYPE="buyer"))
plt.plot(mus, price_mu1, color="green", marker="o", label="Writer")
plt.plot(mus, price_mu2, color="magenta", marker="*", label="Buyer")
plt.xlabel("mu")
plt.ylabel("price")
plt.legend()
plt.show()
[1] Cantarutti, N., Guerra, J., Guerra, M., and Grossinho, M. (2019). Option pricing in exponential Lévy models with transaction costs. ArXiv.
[2] Hodges, S. D. and Neuberger, A. (1989). Optimal replication of contingent claims under transaction costs. The Review of Future Markets, 8(2):222–239.