
EDIT: the previous version of this post contained a small mistake (machine precision related I think). I rewrote it and updated the figures.
As I pointed out earlier, one need to check the code before any use in production. One practical aspect is deciding the number of steps to use. There is obviously a trade off between precision and calculation time.
One also needs to make sure that increasing the number of steps actually decreases the error. We have seen already that for the binomial tree for european option, it is not always the case. We have seen that the price oscillate.
One can see on the following image (taken from that older post):
Pricing without regularization using 15 steps is worse than using 10 steps. For american options, one cannot check against the Black and Scholes price but still convergence is important. In the case the dividend is late, we can see oscillations.
For the following graph, we have $S0=100$, $K=100$, $r=5%$, $\sigma=40%$ and $T=3$. We have a dividend at $T=2.9$ for 1 euro.
In the tree, the dividend is paid on a step but there will be a mismatch between the real dividend date and the step. This makes the price oscillate. As the step size goes toward 0, that mismatch will go toward 0 and the oscillations go away but for smaller number of steps this generates oscillations.
I try two methods to fix this. Once I know the mismatch between the step where the dividend will be paid and the real dividend date I can either:
1/increase the step size so that the step number multiplied by the step size falls right on the dividend date.
2/decrease the step size so that the dividend date falls right onto the next step.
In both cases if I keep the number of steps unchanged, the maturity of the option will be wrong. If I increase the step size I will need less steps for the same maturity, opposite if I decrease the size. And I will need a fractional step to reach exactly the maturity I want. That can pose problem for my recombining tree.
But remember that regularization is replacing the last step of the tree by a BS formula. I can use that and change the time used to have my "fraction" of step. One can see on the following graph the effect of this change. Note that I start plotting from 150 steps.
For a more global view, we see convergence of the 3 methods so, in theory, all methods works.
The adapted time step oscillate but much much less than the original.
Let's now see what happens for european option using such method. This graph shows a large point of view:
and this one is a close up
A few comments are in order:
It seems the adjustment of the time step creates a shift in the european call price. I don't see why at the moment. That difference is really small though. Increasing the time step size seem to introduce a downward bias so I would choose the technique decreasing the size of the time step.
I did a quick check and results are similar with ITM options.
Find enclosed the code for the decreased size version:
- def BinomialTree8c(option, params):
- #we pass two objects: one for the option values and one for the tree parameters
- N=params.N
- [S0,K,sigma,T,r2,divi,american]=[option.S0,option.K,option.sigma,option.T,option.r,option.divi,option.american]
- #calculate delta T
- deltaT = float(T) / N
- #we improve the previous tree by generalization of payoff
- def expiCall(stock,strike):
- return(np.maximum(stock[:]-strike[:][0],0))
- def expiPut(stock,strike):
- return(np.maximum(strike[:][0]-stock[:],0))
- def earlyCall(Option,stock,strike):
- return(np.maximum(stock[:]-strike[:][0],Option))
- def earlyEuro(Option,stock,strike):
- return(np.maximum(Option,0))
- def earlyPut(Option,stock,strike):
- return(np.maximum(strike[:][0]-stock[:],Option))
- #regularisation will be usefull for delta calculations
- def expiCallR(stock,strike,rr=0,vol=sigma,time=deltaT):
- return(BlackScholes('C',stock,strike,rr,vol,time))
- def expiPutR(stock,strike,rr=0,vol=sigma,time=deltaT):
- return(BlackScholes('P',stock,strike,rr,vol,time))
- if american=='false':
- earlyCall=earlyEuro
- earlyPut=earlyEuro
- r=[[],[]]
- dividends=[[],[]]
- #2 columns vector, one for amount one for time.
- #we make sure we don't take into account dividend happening after expiration
- if (np.size(divi)>0 and divi[0][0]<T) :
- lastdiv=np.nonzero(np.array(divi[0][:])<=T)[0][-1]
- dividends[0]=divi[0][:lastdiv+1]
- dividends[1]=divi[1][:lastdiv+1]
- if (params.stepAdapt=='true'and np.size(divi)>0):
- #in order to have the last dividend fall on a step we modify the length of the option
- #we correct via regularisation with a smaller time step
- #first we need the mismatch
- #print deltaT,N,deltaT*N
- TimeM=np.int(dividends[0][-1]/deltaT)
- TimeMismatch=(dividends[0][-1]-TimeM*deltaT)-0.00001*deltaT
- deltaT=deltaT-(deltaT-TimeMismatch)/TimeM
- ExpMismatchInt=int((T-deltaT*N)/deltaT)
- ExpMismatch=ExpMismatchInt-(T-deltaT*N)/deltaT
- N=N+ExpMismatchInt
- #print deltaT,N,deltaT*N,deltaT*N-ExpMismatch*deltaT
- else:
- ExpMismatch=0
- ExpMismatchInt=0
- #BurnIn are 2 extra steps we add.
- #that's used for calculation of delta,
- if params.BurnIn=='true':
- BI=2
- N=N+BI
- else:
- BI=0
- #similar structure for rates
- # rates with date d1 is use until date d1 so we need the first date post maturity.
- if np.size(r2)>0 :
- lastrate=np.nonzero(np.array(r2[0][:])<=T)[0][-1]
- r[0]=r2[0][:lastrate+2]
- r[1]=r2[1][:lastrate+2]
- #Transform the dividend date into a step
- if np.size(dividends)>0:
- dividendsStep=np.floor(np.multiply(dividends[0],1/deltaT))
- else:
- dividendsStep=[]
- if np.size(r)>0:
- rateChangeStep=np.floor(np.multiply(r[0],1/deltaT))
- else:
- rateChangeStep=[]
- #we add the BurnIn to the dividend step for correction
- dividendsStep=dividendsStep+np.asarray( [float(BI) for i in xrange(np.size(dividends[1][:]))])
- rateChangeStep=rateChangeStep+np.asarray( [float(BI) for i in xrange(np.size(r[1][:]))])
- #prepare the array for discount factors used for the discounting of dividends
- DFArray=np.zeros((N+2,1),float)
- rArray=np.zeros((N+2,1),float)
- #get the steps where the rate changes
- prevstep=0
- prevrate=0
- for step in range(len(rateChangeStep)-1):
- DFArray[prevstep:rateChangeStep[step]]=np.exp(-prevrate*deltaT)
- DFArray[rateChangeStep[step]:]=np.exp(-r[1][step]*deltaT)
- rArray[prevstep:rateChangeStep[step],0]=prevrate
- rArray[rateChangeStep[step]:,0]=r[1][step]
- prevstep=rateChangeStep[step]
- prevrate=r[1][step]
- rArray[0]=rArray[1]
- pvdividends=0
- #get present value of the dividend
- if np.size(dividends)>0:
- pvdividends=np.sum(dividends[1])
- #for index,dv in enumerate(list(dividends[1])):
- # pvdividends=pvdividends+dv*np.product(DFArray[0:np.int_(dividendsStep[index])])
- else:
- pvdividends=0
- S0=S0-pvdividends
- currentDividend=0
- # up and down factor will be constant for the tree so we calculate outside the loop
- u = np.exp(sigma * np.sqrt(deltaT))
- d = 1.0 / u
- #if we use regularisation, one need one less time step as we use BS for the last one.
- if params.regul=='true':
- N=N-1
- #to work with vector we need to init the arrays using numpy
- # we do call and put together
- fsC = np.asarray([0.0 for k in xrange(N + 1)])
- fsP = np.asarray([0.0 for k in xrange(N + 1)])
- #we need the stock tree for calculations of expiration values
- fs2 = np.asarray([(S0 * u**k * d**(N - k)) for k in xrange(N + 1)])
- #we vectorize the strikes as well so the expiration check will be faster
- fs3 =np.asarray( [float(K) for k in xrange(N + 1)])
- # Compute the leaves, f_{N, j}
- if params.regul=='true':
- fsC = expiCallR(fs2,fs3,rArray[-1][0],sigma,(1.0-ExpMismatch)*deltaT)
- fsP = expiPutR(fs2,fs3,rArray[-1][0],sigma,(1.0-ExpMismatch)*deltaT)
- else:
- fsC = expiCall(fs2,fs3)
- fsP = expiPut(fs2,fs3)
- #we get the rate
- df=rArray[N-1]*deltaT
- discountfactor=np.exp(-df)
- p=(np.exp(df)-d)/(u-d)
- divi=0
- #calculate backward the option prices
- #if we use BurnIn, we don't need to calculate the extra steps for the price
- #but we need them for theta
- for k in xrange(N-1, -1+BI, -1):
- if (k in rateChangeStep[:]):
- df= (r[1][np.nonzero(rateChangeStep==(k))[0]-1])*deltaT
- discountfactor= np.exp(-df)
- p=(np.exp(df)-d)/(u-d)
- fsC[:-1]=discountfactor* (p * fsC[1:] + (1-p) * fsC[:-1])
- fsP[:-1]=discountfactor* (p * fsP[1:] + (1-p) * fsP[:-1])
- fs2[:]=fs2[:]*u
- #we add back the dividends in current dividends as we move backward, useful
- #only for american options
- if (k in dividendsStep):
- divi=dividends[1][np.nonzero(dividendsStep==(k))[0]]
- currentDividend=currentDividend+divi
- #the VN method requires an interpolation
- tckC=si.splrep(fs2[:],fsC[:],k=1)
- tckP=si.splrep(fs2[:],fsP[:],k=1)
- #stock tree is recalculated as if the dividends were in from the start
- #carreful with indices we are in step i so we calculated N-i steps already
- fs2[:]=np.asarray([((S0+currentDividend) * u**(j) * d**(- j+k)) for j in xrange(N + 1)])
- #option prices are interpolated on this tree and backward calculations continues
- fsC[:]=si.splev(fs2[:]-divi,tckC,der=0)
- fsP[:]=si.splev(fs2[:]-divi,tckP,der=0)
- fsC[:]=earlyCall(fsC[:],fs2[:],fs3[:])
- fsP[:]=earlyPut(fsP[:],fs2[:],fs3[:])
- if params.BurnIn=='true':
- option.call.price=fsC[BI/2]
- option.put.price=fsP[BI/2]
- option.call.delta=(fsC[BI/2+1]-fsC[BI/2-1])/(fs2[BI/2+1]-fs2[BI/2-1])
- option.put.delta=(fsP[BI/2+1]-fsP[BI/2-1])/(fs2[BI/2+1]-fs2[BI/2-1])
- s2a=fs2[BI/2+1]-fs2[BI/2]
- s2b=fs2[BI/2]-fs2[BI/2-1]
- option.call.gamma=((fsC[BI/2+1]-fsC[BI/2])/s2a +(-fsC[BI/2]+fsC[BI/2-1])/s2b)/((fs2[BI/2+1]-fs2[BI/2-1])/2)
- option.put.gamma=((fsP[BI/2+1]-fsP[BI/2])/s2a +(-fsP[BI/2]+fsP[BI/2-1])/s2b)/((fs2[BI/2+1]-fs2[BI/2-1])/2)
- #final steps are done outside the loop for theta calculation only,
- #no american, rate or divs in there
- for i in xrange(-1+BI, -1, -1):
- fsC[:-1]=discountfactor* (p * fsC[1:] + (1-p) * fsC[:-1])
- fsP[:-1]=discountfactor* (p * fsP[1:] + (1-p) * fsP[:-1])
- option.call.theta=(option.call.price-fsC[0])/(BI*deltaT)
- option.put.theta=(option.put.price-fsP[0])/(BI*deltaT)
- else:
- option.call.price=fsC[0]
- option.put.price=fsP[0]
- return option
In fact not much changes: the size time step is adapted in a specific if statement, the time used in the regularization as well. The rest stays all the same.
Note that if I have several late dividends, the method will adjust only to the latest one, one could still see oscillations. Here with dividends at 2.8 and 2.9
Aucun commentaire:
Enregistrer un commentaire