· 5 years ago · Jun 01, 2020, 05:12 AM
1'''
2References:
3- https://jakevdp.github.io/PythonDataScienceHandbook/04.12-three-dimensional-plotting.html
4- https://stackoverflow.com/questions/42677160/matplotlib-3d-scatter-plot-date
5- https://en.wikipedia.org/wiki/Greeks_(finance)
6- https://en.wikipedia.org/wiki/Bisection_method
7- https://quant.stackexchange.com/questions/7761/a-simple-formula-for-calculating-implied-volatility
8- https://www.risklatte.xyz/Articles/QuantitativeFinance/QF135.php
9'''
10from scipy.stats import norm
11from mpl_toolkits import mplot3d
12import yfinance as yf
13import numpy as np
14import datetime as dt
15import math as m
16import sys
17
18import matplotlib.pyplot as plt
19import matplotlib.backends.backend_tkagg
20import warnings
21warnings.filterwarnings("ignore", module="matplotlib")
22plt.switch_backend('TkAgg')
23
24
25class Option:
26
27 # TODO Make the impliedPrices function in terms of SD
28 # TODO Create function that steps the contract through time, IV, and S
29
30 def __init__(self, current_date, current_time, opt_type, exp, V, S, K, r, volume, openInterest, bid, ask):
31 '''
32 Sets all the attributes of the contract.
33 '''
34 self.opt_type = opt_type.lower()
35
36 if self.opt_type == 'call':
37
38 if K < S:
39 self.itm = True
40
41 else:
42 self.itm = False
43
44 elif self.opt_type == 'put':
45
46 if S < K:
47 self.itm = True
48
49 else:
50 self.itm = False
51
52 self.exp = exp
53 self.V = round(V, 2)
54 self.S = round(S, 2)
55 self.K = round(K, 2)
56 self.date = current_date
57 self.time = current_time
58 self.exp = exp
59 self.t = self.__t(current_date, current_time, exp)
60 self.r = r
61 self.q = 0
62 self.volume = volume
63 self.openInterest = openInterest
64 self.bid = bid
65 self.ask = ask
66 vol_params = self.__BSMIV(self.S, self.t)
67 self.IV = vol_params[0]
68 self.vega = vol_params[1]
69 self.delta = self.__BSMdelta()
70 self.gamma = self.__BSMgamma()
71 self.theta = self.__BSMtheta()
72 self.rho = self.__BSMrho()
73 self.Lambda = self.__BSMlambda()
74 self.vanna = self.__BSMvanna()
75 self.charm = self.__BSMcharm()
76 self.vomma = self.__BSMvomma()
77 self.veta = self.__BSMveta()
78 self.speed = self.__BSMspeed()
79 self.zomma = self.__BSMzomma()
80 self.color = self.__BSMcolor()
81 self.ultima = self.__BSMultima()
82
83 def __repr__(self):
84 '''
85 Basic contract information.
86 '''
87 return '${:.2f} strike {} option expiring {}.'.format(self.K,
88 self.opt_type,
89 self.exp)
90
91 def __t(self, current_date, current_time, exp):
92 '''
93 Calculates time left to expiration in years.
94 '''
95 hr, minute = 17, 30
96 year, month, day = [int(x) for x in exp.split('-')]
97 exp_dt = dt.datetime(year, month, day, hr, minute)
98
99 hr, minute = [int(x) for x in current_time.split(':')]
100 year, month, day = [int(x) for x in current_date.split('-')]
101 current_dt = dt.datetime(year, month, day, hr, minute)
102
103 days = 24*60*60*(exp_dt - current_dt).days
104 seconds = (exp_dt - current_dt).seconds
105
106 return (days + seconds) / (365*24*60*60)
107
108 def __d1(self, S, t, v):
109 '''
110 Struggling to come up with a good explanation.
111 It's an input into the cumulative distribution function.
112 '''
113 K = self.K
114 r = self.r
115 q = self.q
116
117 return ((m.log(S / K) + (r - q + 0.5*v**2)*t)) / (v*m.sqrt(t))
118
119 def __d2(self, S, t, v):
120 '''
121 Struggling to come up with a good explanation.
122 It's an input into the cumulative distribution function.
123 '''
124 d1 = self.__d1(S, t, v)
125
126 return d1 - v*m.sqrt(t)
127
128 def __pvK(self, t):
129 '''
130 Present value (pv) of the strike (K)
131 '''
132 K = self.K
133 r = self.r
134 return K*m.exp(-r*t)
135
136 def __pvS(self, S, t):
137 '''
138 Present value (pv) of the stock price (S)
139 '''
140 q = self.q
141
142 return S*m.exp(-q*t)
143
144 def __phi(self, x): return norm.pdf(x)
145
146 def __N(self, x): return norm.cdf(x)
147
148 def __BSMprice(self, S, t, v):
149 '''
150 Black-Scholes-Merton price of a call or put.
151 '''
152 K = self.K
153 pvK = self.__pvK(t)
154 pvS = self.__pvS(S, t)
155
156 if t != 0:
157
158 if self.opt_type == 'call':
159 Nd1 = self.__N(self.__d1(S, t, v))
160 Nd2 = -self.__N(self.__d2(S, t, v))
161
162 elif self.opt_type == 'put':
163 Nd1 = -self.__N(-self.__d1(S, t, v))
164 Nd2 = self.__N(-self.__d2(S, t, v))
165
166 return round(pvS*Nd1 + pvK*Nd2, 2)
167
168 elif t == 0:
169
170 if self.opt_type == 'call':
171 intrinsic_value = max(0, S - K)
172
173 elif self.opt_type == 'put':
174 intrinsic_value = max(0, K - S)
175
176 return round(intrinsic_value, 2)
177
178 # First order greek
179 def __BSMvega(self, S, t, v):
180 '''
181 First derivative of the option price (V) with respect to implied volatility (v or IV).
182 - For a 1% increase in IV, how much will the option price rise?
183 '''
184 pvK = self.__pvK(t)
185 phid2 = self.__phi(self.__d2(S, t, v))
186
187 return round((pvK*phid2*m.sqrt(t)) / 100, 4)
188
189 def __BSMIV(self, S, t):
190 '''
191 Volatility implied by the price of the option (v/IV).
192 - For the option price (V) to be fair, how volatile does the underlying (S) need to be?
193 '''
194 V = self.V
195 # K = self.K
196
197 # Bisection method
198
199 # We are trying to solve the error function which is the difference between the estimated price and the actual price
200 # V_est - V
201 # We need to generate two initial guesses at IV, one with a positive error and one with a negative error
202 # Should be reasonable to assume that vol will be somewhere between 1% and 1500%
203 v_lo = 0
204 v_hi = 20
205 v_mid = 0.5*(v_lo + v_hi)
206 V_mid = self.__BSMprice(S, t, v_mid)
207 error = V_mid - V
208 while v_hi - v_lo > 0.1 / 100:
209 if error > 0:
210 v_hi = v_mid
211 elif error < 0:
212 v_lo = v_mid
213 elif error == 0:
214 break
215 v_mid = 0.5*(v_hi + v_lo)
216 V_mid = self.__BSMprice(S, t, v_mid)
217 error = V_mid - V
218 vega = self.__BSMvega(S, t, v_mid)
219
220 # Newton-Raphson method
221 # v = (m.sqrt(2*m.pi) / t)*(V / S)
222 # min_err = 1000000
223 # best_v = 0
224 # best_vega = 0
225
226 # i = 0
227 # if self.opt_type == 'call':
228 # if V < S - K:
229 # V = S - K
230
231 # elif self.opt_type == 'put':
232 # if V < K - S:
233 # V = K - S
234
235 # while i < 5000:
236 # V_est = max(self.__BSMprice(S, t, v), 0.01)
237 # vega = max(self.__BSMvega(S, t, v), 0.0001)
238 # error = V_est - V
239
240 # if abs(error) < min_err:
241 # min_err = abs(error)
242 # best_v = v
243 # best_vega = vega
244
245 # if error == 0:
246 # break
247
248 # else:
249 # v = v - (error/(vega*100))*0.25
250
251 # if (i == 4999) & (error != 0):
252 # print('error in IV loop')
253
254 # i += 1
255
256 # return round(best_v, 4), round(best_vega, 4)
257 return round(v_mid, 4), round(vega, 4)
258
259 # First order greek
260 def __BSMdelta(self):
261 '''
262 First derivative of the option price (V) with respect to
263 the underlying price (S).
264 - For a $1 increase in S, how much will V rise?
265 - Also is the risk neutral probability S is at or below K by expiration.
266 Note that a risk neutral probability is not a real life probability.
267 It is simply the probability that would exist if it were possible to
268 create a completely risk free portfolio with this option, some stock, and
269 some cash.
270 '''
271 S = self.S
272 t = self.t
273 v = self.IV
274
275 if self.opt_type == 'call':
276 Nd1 = self.__N(self.__d1(S, t, v))
277
278 elif self.opt_type == 'put':
279 Nd1 = -self.__N(-self.__d1(S, t, v))
280
281 return round(Nd1, 4)
282
283 # Second order greek
284 def __BSMgamma(self):
285 '''
286 First dertivative of delta with respect to the price of the underlying (S).
287 Second derivative of the option price (V) with respect to the stock price.
288 - For a $1 increase in the stock price, how much will delta increase?
289 '''
290 S = self.S
291 t = self.t
292 v = self.IV
293 pvK = self.__pvK(t)
294 phid2 = self.__phi(self.__d2(S, t, v))
295
296 return round((pvK*phid2) / (S**2*v*m.sqrt(t)), 4)
297
298 # First order greek
299 def __BSMtheta(self):
300 '''
301 First derivative of the option price (V) with respect to time (t).
302 - How much less will the option be worth tomorrow?
303 '''
304 S = self.S
305 t = self.t
306 v = self.IV
307 pvK = self.__pvK(t)
308 pvS = self.__pvS(S, t)
309
310 if self.opt_type == 'call':
311 phid1 = self.__phi(self.__d1(S, t, v))
312 r = -self.r
313 q = self.q
314 Nd1 = self.__N(self.__d1(S, t, v))
315 Nd2 = self.__N(self.__d2(S, t, v))
316
317 elif self.opt_type == 'put':
318 phid1 = self.__phi(-self.__d1(S, t, v))
319 r = self.r
320 q = -self.q
321 Nd1 = self.__N(-self.__d1(S, t, v))
322 Nd2 = self.__N(-self.__d2(S, t, v))
323
324 return round((-((pvS*phid1*v) / (2*m.sqrt(t))) + r*pvK*Nd2 + q*pvS*Nd1) / 365, 4)
325
326 # First order greek
327 def __BSMrho(self):
328 '''
329 First derivative of the option price (V) with respect to the risk free interest rate (r).
330 - For a 1% change in interest rates, by how many dollars will the value of the option change?
331 '''
332 S = self.S
333 t = self.t
334 v = self.IV
335
336 if self.opt_type == 'call':
337 pvK = self.__pvK(t)
338 Nd2 = self.__N(self.__d2(S, t, v))
339
340 elif self.opt_type == 'put':
341 pvK = -self.__pvK(t)
342 Nd2 = self.__N(-self.__d2(S, t, v))
343
344 return round((pvK*t*Nd2) / 100, 4)
345
346 # First order greek
347 def __BSMlambda(self):
348 '''
349 Measures the percentage change in the option price (V) per percentage change
350 in the price of the underlying (S).
351 - How much leverage does this option have?
352 '''
353 V = self.V
354 S = self.S
355 delta = self.delta
356
357 return round(delta*(S / V), 4)
358
359 # Second order greek
360 def __BSMvanna(self):
361 '''
362 First derivative of delta with respect to implied volatility.
363 - If volatility changes by 1%, how much will delta change?
364 '''
365 V = self.V
366 S = self.S
367 t = self.t
368 v = self.IV
369 d1 = self.__d1(S, t, v)
370
371 return round((V / S)*(1 - (d1 / (v*m.sqrt(t)))), 4)
372
373 # Second order greek
374 def __BSMcharm(self):
375 '''
376 First derivative of delta with respect to time.
377 - How much different will delta be tomorrow if everything else stays the same?
378 - Also can think of it as 'delta decay'
379 '''
380 S = self.S
381 t = self.t
382 r = self.r
383 v = self.IV
384 pv = m.exp(-self.q*t)
385 phid1 = self.__phi(self.__d1(S, t, v))
386 d2 = self.__d2(S, t, v)
387 mess = (2*(r - self.q)*t - d2*v*m.sqrt(t)) / (2*t*v*m.sqrt(t))
388
389 if self.opt_type == 'call':
390 q = self.q
391 Nd1 = self.__N(self.__d1(S, t, v))
392
393 elif self.opt_type == 'put':
394 q = -self.q
395 Nd1 = self.__N(-self.__d1(S, t, v))
396
397 return round((q*pv*Nd1 - pv*phid1*mess) / 365, 4)
398
399 # Second order greek
400 def __BSMvomma(self):
401 '''
402 First derivative of vega with respect to implied volatility.
403 Also the second derivative of the option price (V) with respect to
404 implied volatility.
405 - If IV changes by 1%, how will vega change?
406 '''
407 S = self.S
408 t = self.t
409 vega = self.vega
410 v = self.IV
411 d1 = self.__d1(S, t, v)
412 d2 = self.__d2(S, t, v)
413
414 return round((vega*d1*d2) / v, 4)
415
416 # Second order greek
417 def __BSMveta(self):
418 '''
419 First derivative of vega with respect to time (t).
420 - How much different will vega be tomorrow if everything else stays the same?
421 '''
422 S = self.S
423 t = self.t
424 v = self.IV
425 pvS = self.__pvS(S, t,)
426 d1 = self.__d1(S, t, v)
427 d2 = self.__d2(S, t, v)
428 phid1 = self.__phi(d1)
429 r = self.r
430 q = self.q
431 mess1 = ((r - q)*d1) / (v*m.sqrt(t))
432 mess2 = (1 + d1*d2) / (2*t)
433
434 return round((-pvS*phid1*m.sqrt(t)*(q + mess1 - mess2)) / (100*365), 4)
435
436 # Third order greek
437 def __BSMspeed(self):
438 '''
439 First derivative of gamma with respect to the underlying price (S).
440 - If S increases by $1, how will gamma change?
441 '''
442 gamma = self.gamma
443 S = self.S
444 v = self.IV
445 t = self.t
446 d1 = self.__d1(S, t, v)
447
448 return round(-(gamma / S)*((d1 / (v*m.sqrt(t))) + 1), 4)
449
450 # Third order greek
451 def __BSMzomma(self):
452 '''
453 First derivative of gamma with respect to implied volatility.
454 - If volatility changes by 1%, how will gamma change?
455 '''
456 gamma = self.gamma
457 S = self.S
458 t = self.t
459 v = self.IV
460 d1 = self.__d1(S, t, v)
461 d2 = self.__d2(S, t, v)
462
463 return round(gamma*((d1*d2 - 1) / v), 4)
464
465 # Third order greek
466 def __BSMcolor(self):
467 '''
468 First derivative of gamma with respect to time.
469 - How much different will gamma be tomorrow if everything else stays the same?
470 '''
471 S = self.S
472 t = self.t
473 r = self.r
474 v = self.IV
475 q = self.q
476 pv = m.exp(-q*t)
477 d1 = self.__d1(S, t, v)
478 d2 = self.__d2(S, t, v)
479 phid1 = self.__phi(d1)
480 mess = ((2*(r - q)*t - d2*v*m.sqrt(t)) / (v*m.sqrt(t)))*d1
481
482 return round((-pv*(phid1 / (2*S*t*v*m.sqrt(t)))*(2*q*t + 1 + mess)) / 365, 4)
483
484 # Third order greek
485 def __BSMultima(self):
486 '''
487 First derivative of vomma with respect to volatility.
488 - ...why? At this point it just seems like an exercise in calculus.
489 '''
490 vega = self.vega
491 S = self.S
492 t = self.t
493 v = self.IV
494 d1 = self.__d1(S, t, v)
495 d2 = self.__d2(S, t, v)
496
497 return round((-vega/(v**2))*(d1*d2*(1 - d1*d2) + d1**2 + d2**2), 4)
498
499 def theoPrice(self, date, S, v):
500 '''
501 Calculates the theoretical price of the option given:
502 - date 'YYYY-MM-DD'
503 - underlying price (S)
504 - implied volatility (v)
505 '''
506 year, month, day = date.split('-')
507 date = dt.datetime(int(year), int(month), int(day))
508 year, month, day = self.exp.split('-')
509 exp = dt.datetime(int(year), int(month), int(day))
510
511 t = (exp - date).days / 365
512
513 return self.__BSMprice(S, t, v)
514
515 def theoPnL(self, date, S, v):
516 '''
517 Calculates the theoretical profit/loss given:
518 - date 'YYYY-MM-DD'
519 - underlying price (S)
520 - implied volatility (v)
521 '''
522 return round(self.theoPrice(date, S, v) - self.V, 2)
523
524 def impliedPrices(self, show):
525 '''
526 Returns a tuple containing two lists.
527 - list[0] = dates from tomorrow until expiration
528 - list[1] = price on each date
529
530 - show = True | plots prices over time
531 - show = False | does not plot prices over time
532
533 If implied volatility is 20% for an option expiring in 1 year, this means that
534 the market is implying 1 year from now, there is roughly a 68% chance the underlying
535 will be 20% higher or lower than it currently is.
536
537 We can scale this annual number to any timeframe of interest according to v*sqrt(days/365).
538 The denominator is 365 because calendar days are used for simplicity.
539
540 If only trading days were to be taken into account, the denominator would be 252.
541 '''
542 S = self.S
543 v = self.IV
544 t = self.t
545
546 days = [i + 1 for i in range(int(t*365))]
547
548 if self.opt_type == 'call':
549 prices = [round(S + v*m.sqrt(day / 365)*S, 2) for day in days]
550
551 elif self.opt_type == 'put':
552 prices = [round(S - v*m.sqrt(day / 365)*S, 2) for day in days]
553
554 today = dt.datetime.now()
555 dates = [(today + dt.timedelta(days=day)).date() for day in days]
556
557 if show == True:
558 plt.title('Implied moves according to a:\n{}\nLast price: ${:.2f} | IV: {:.2f}%'.format(
559 self.__repr__(), self.V, 100*self.IV))
560
561 plt.xlabel('Date')
562 plt.ylabel('Spot price ($)')
563 plt.plot(dates, prices)
564 plt.show()
565
566 return dates, prices
567
568
569'''
570Plotting section. Apologies in advance for little to no comments.
571'''
572
573
574def multi_plot_input():
575 '''
576 User input
577 '''
578 # Get ticker
579 ticker_string = '\nEnter ticker symbol: '
580 ticker = input(ticker_string).upper()
581
582 try:
583 yf.Ticker(ticker).options[0]
584
585 except:
586 stop_loop = 0
587
588 while stop_loop == 0:
589 print('Ticker symbol is either invalid or not optionable.')
590 ticker = input(ticker_string).upper()
591
592 try:
593 yf.Ticker(ticker).options[0]
594 stop_loop = 1
595
596 except:
597 stop_loop = 0
598
599 print('\nStandard Parameters | Nonstandard Parameters')
600 print(' last or mid price | rho [dV/dr]')
601 print(' IV | charm [ddelta/dt]')
602 print(' delta | veta [dvega/dt]')
603 print(' theta | color [dgamma/dt]')
604 print(' volume | speed [dgamma/dS]')
605 print(' vega | vanna [ddelta/dv]')
606 print(' gamma | vomma [dvega/dv]')
607 print(' Open Interest | zomma [dgamma/dv]\n')
608
609 param_string = 'Enter 0 for standard parameters, 1 for nonstandard: '
610 param_type = input(param_string)
611
612 while param_type not in ['0', '1']:
613 param_type = input(param_string)
614
615 if param_type == '0':
616 params = [
617 'V',
618 'IV',
619 'delta',
620 'theta',
621 'volume',
622 'vega',
623 'gamma',
624 'openInterest'
625 ]
626
627 elif param_type == '1':
628 print('\n**Warning: accuracy of higher order greeks has not been verified**\n')
629 params = [
630 'rho',
631 'charm',
632 'veta',
633 'color',
634 'speed',
635 'vanna',
636 'vomma',
637 'zomma'
638 ]
639
640 # Get price type
641 price_type = input('Enter price to use for calcs [mid or last]: ')
642
643 while price_type not in ['mid', 'last']:
644 price_type = input('Enter price to use for calcs [mid or last]: ')
645
646 # Get option type
647 opt_type_string = 'Enter option type [calls, puts, or both]: '
648 opt_type = input(opt_type_string)
649
650 while opt_type not in ['calls', 'puts', 'both']:
651 opt_type = input(opt_type_string)
652
653 # Get risk free rate
654 stop_loop = 0
655 while stop_loop == 0:
656 r = input('Enter the risk-free rate: ')
657 try:
658 r = float(r)
659 stop_loop = 1
660 except:
661 continue
662
663 # Get date/time
664 datetime_options = ['0', '1']
665
666 print('\n"Time" refers to the time to use for the time to expiration calculation.')
667 print('Example: if it is currently the weekend, and you want to see the metrics')
668 print('based on EOD Friday (which is what the prices will be from), enter "1",')
669 print('and enter the date of the most recent Friday, with 16:00 as the time (4pm).\n')
670 which_datetime_string = 'Enter 0 to use current date/time, 1 to specify date/time: '
671 which_datetime = input(which_datetime_string)
672
673 while which_datetime not in datetime_options:
674 which_datetime = input(which_datetime_string)
675
676 if which_datetime == '0':
677 now = dt.datetime.now()
678 current_date = str(now.date())
679 current_time = '{}:{}'.format(now.time().hour, now.time().minute)
680
681 elif which_datetime == '1':
682 current_date_string = 'Enter current date [YYYY-MM-DD]: '
683 current_date = input(current_date_string)
684
685 try:
686 year, month, day = [int(x) for x in current_date.split('-')]
687 dt.datetime(year, month, day)
688
689 except:
690 stop_loop = 0
691
692 while stop_loop == 0:
693 current_date = input(current_date_string)
694
695 try:
696 year, month, day = [int(x)
697 for x in current_date.split('-')]
698 dt.datetime(year, month, day)
699 stop_loop = 1
700
701 except:
702 stop_loop = 0
703
704 current_time_string = 'Enter current 24H time [HH:MM]: '
705 current_time = input(current_time_string)
706
707 try:
708 hour, minute = [int(x) for x in current_time.split(':')]
709 dt.datetime(year, month, day, hour, minute)
710
711 except:
712 stop_loop = 0
713
714 while stop_loop == 0:
715 current_time = input(current_time_string)
716
717 try:
718 hour, minute = [int(x) for x in current_time.split(':')]
719 dt.datetime(year, month, day, hour, minute)
720 stop_loop = 1
721
722 except:
723 stop_loop = 0
724
725 return ticker, params, price_type, opt_type, current_date, current_time, r
726
727
728def get_options(current_date, current_time, ticker, opt_type, price_type, r):
729
730 # Get ticker object
731 ticker = yf.Ticker(ticker)
732
733 S = ticker.info['regularMarketPrice']
734
735 # Get exps
736 exps = ticker.options
737
738 # This dict will hold all the objects across dates
739 call_objects = {}
740 put_objects = {}
741
742 # Get option chains
743 for exp in exps:
744 year, month, day = [int(x) for x in exp.split('-')]
745 corrected_exp = str(
746 (dt.datetime(year, month, day) + dt.timedelta(days=1)).date())
747
748 print('Getting data for options expiring {}...'.format(corrected_exp))
749
750 option_chain = ticker.option_chain(exp)
751
752 calls = option_chain.calls.fillna(0)
753 puts = option_chain.puts.fillna(0)
754
755 del option_chain
756
757 single_call_chain = []
758 single_put_chain = []
759
760 # Convert each option in chain into an option object (from BSMoption_v4)
761 for i in range(len(calls)):
762
763 K = calls['strike'].iloc[i]
764 volume = calls['volume'].iloc[i]
765 openInterest = calls['openInterest'].iloc[i]
766 bid = calls['bid'].iloc[i]
767 ask = calls['ask'].iloc[i]
768 last = calls['lastPrice'].iloc[i]
769
770 if price_type == 'mid':
771 V = max(round((bid + ask) / 2, 2), 0.01)
772
773 else:
774 V = max(last, 0.01)
775
776 single_call_chain.append(Option(current_date,
777 current_time,
778 'call',
779 corrected_exp,
780 V, S, K, r,
781 volume,
782 openInterest,
783 bid, ask))
784
785 for i in range(len(puts)):
786
787 K = puts['strike'].iloc[i]
788 volume = puts['volume'].iloc[i]
789 openInterest = puts['openInterest'].iloc[i]
790 bid = puts['bid'].iloc[i]
791 ask = puts['ask'].iloc[i]
792 last = puts['lastPrice'].iloc[i]
793
794 if price_type == 'mid':
795 V = max(round((bid + ask) / 2, 2), 0.01)
796
797 else:
798 V = max(last, 0.01)
799
800 single_put_chain.append(Option(current_date,
801 current_time,
802 'put',
803 corrected_exp,
804 V, S, K, r,
805 volume,
806 openInterest,
807 bid, ask))
808
809 # Add objects to dicts
810 call_objects['{} {} options'.format(
811 corrected_exp, 'call')] = single_call_chain
812
813 put_objects['{} {} options'.format(
814 corrected_exp, 'put')] = single_put_chain
815
816 del calls
817 del puts
818
819 if opt_type == 'calls':
820 return {'calls': call_objects}
821
822 elif opt_type == 'puts':
823 return {'puts': put_objects}
824
825 else:
826 return {'calls': call_objects, 'puts': put_objects}
827
828
829def generate_plots(ticker, options, params, price_type, current_date, current_time):
830 # Set chart layout
831 layout = [2, 4]
832 rows = layout[0]
833 cols = layout[1]
834 fig = plt.figure()
835
836 # Create list of expirations
837 exps = []
838
839 # Set titles that can vary
840 titles = [x.capitalize() if x != 'IV' else x for x in params]
841
842 if 'V' in titles:
843 titles[params.index('V')] = price_type.capitalize() + ' Price'
844
845 if 'volume_OI' in titles:
846 titles[params.index('volume_OI')] = 'Volume/Open Interest'
847
848 opt_types = list(options.keys())
849 first_iter = True
850 k = 0
851 main_title = []
852 # Loop through calls and puts
853 for opt_type in opt_types:
854 # print(opt_type)
855
856 if opt_type == 'calls':
857 itm_color = 'green'
858 otm_color = 'blue'
859 main_title.append(
860 '\nITM calls={}, OTM calls={}'.format(itm_color, otm_color))
861
862 else:
863 itm_color = 'red'
864 otm_color = 'purple'
865 main_title.append(
866 '\nITM puts={}, OTM puts={}'.format(itm_color, otm_color))
867
868 option_chains = options[opt_type]
869 single_chains = list(option_chains.keys())
870
871 j = 0
872 # Loop through expirations
873 for chain in single_chains:
874 # print(chain)
875 if k == 0:
876 exp = chain.split(' ')[0]
877 exps.append(exp[0:])
878
879 single_exp_options = option_chains[chain]
880 i = 1
881
882 # Loop through params
883 for param in params:
884 x_itm = []
885 y_itm = []
886 z_itm = []
887
888 x_otm = []
889 y_otm = []
890 z_otm = []
891
892 if first_iter == True:
893 # print('Create axis {}:{} {} {}'.format(
894 # i, exp, opt_type[0:-1], param))
895 exec("ax{} = fig.add_subplot({}{}{}, projection='3d')".format(i,
896 rows,
897 cols,
898 i))
899 eval("ax{}.set_title(titles[{}])".format(i, i - 1))
900 eval("ax{}.set_xlabel('strike')".format(i))
901 eval("ax{}.view_init(45, -65)".format(i))
902
903 else:
904 # print('Plot on axis {}:{} {} {}'.format(
905 # i, exp, opt_type[0:-1], param))
906 pass
907 # Loop through each option in the expiration
908 for option in single_exp_options:
909 # print(option)
910 S = option.S
911 itm = option.itm
912
913 if itm == True:
914 x_itm.append(option.K)
915 y_itm.append(j)
916 eval('z_itm.append(option.{})'.format(param))
917
918 else:
919 x_otm.append(option.K)
920 y_otm.append(j)
921 eval('z_otm.append(option.{})'.format(param))
922
923 eval("ax{}.plot(x_itm, y_itm, z_itm, '{}')".format(
924 i, itm_color))
925 eval("ax{}.plot(x_otm, y_otm, z_otm, '{}')".format(
926 i, otm_color))
927 i += 1
928
929 first_iter = False
930 j += 1
931
932 k += 1
933
934 if j <= 13:
935 ticks = [x for x in range(j)]
936 else:
937 ticks = [x for x in range(1, j, 2)]
938 exps = [exps[x] for x in ticks]
939
940 axs = [x for x in range(1, i)]
941
942 main_title.insert(0, 'ATM {}: ${:.2f} [{} {}]'.format(
943 ticker, S, current_date, current_time))
944
945 fig.suptitle(''.join(main_title))
946
947 for ax in axs:
948 eval("ax{}.yaxis.set_ticks(ticks)".format(ax))
949 eval("ax{}.yaxis.set_ticklabels(exps, fontsize=8, verticalalignment='baseline', horizontalalignment='center', rotation=-20)".format(ax))
950
951 fig.tight_layout(h_pad=1, w_pad=0.001)
952 plt.subplots_adjust(wspace=0.001, hspace=0.1)
953 plt.draw()
954 plt.show()
955
956 pass
957
958
959def main():
960 stop_main_loop = '0'
961 print('\n------------------------------------READ ME----------------------------------------------\n')
962 print('Data is pulled from Yahoo! finance with the yfinance python package. There is a rate')
963 print('limit of 2000 request per hour to the Yahoo! API, and each expiration in an option')
964 print('chain requires 1 API call. So if you are running this a lot, you may get odd behavior.\n')
965 print('Yahoo! also provides implied volatility numbers, but in the spirit of this exercise,')
966 print('IVs are manually calculated from the option price using a bisection algorithm.\n')
967 print('Bisection is slower than something like Newton-Raphson, but is guaranteed to converge')
968 print('given that the answer is between the initial guesses. The initial guesses for IV in')
969 print('this script are 0% and 2000%. In the nearly impossible event that IV is greater than')
970 print('2000%, funny stuff will happen. Funny stuff can also happen for deep ITM options trading')
971 print('under intrinsic value.\n')
972 print('There are no filters with respect to open interest or volume. Whatever bid/ask/last')
973 print('is present will be used.')
974 print('\nI also realize for underlyings with many expirations, the "date" axis labels become')
975 print('crowded and hard to see. For tickers with more than 13 expirations, only every other')
976 print('expiration will be labeled on the plots.')
977 print('\n-----------------------------------------------------------------------------------------\n')
978 input('Press any key to continue...\n')
979 while stop_main_loop == '0':
980
981 mode = input(
982 'Press 0 to make plots, 1 to calculate the standard greeks for a single option: ')
983 while mode not in ['0', '1']:
984 mode = input(
985 'Press 0 to make plots, 1 to calculate the standard greeks for a single option: ')
986
987 if mode == '0':
988 ticker, params, price_type, opt_type, current_date, current_time, r = multi_plot_input()
989
990 print('Connecting to Yahoo!...')
991
992 options = get_options(current_date, current_time,
993 ticker, opt_type, price_type, r)
994
995 generate_plots(ticker, options, params, price_type,
996 current_date, current_time)
997 stop_main_loop = input(
998 'Enter 0 to continue, anything else to quit: ')
999
1000 elif mode == '1':
1001
1002 opt_type = input('Enter option type [put or call]: ')
1003 while opt_type not in ['put', 'call']:
1004 opt_type = input('Enter option type [put or call]: ')
1005
1006 exp = input('Enter expiration date [YYYY-MM-DD]: ')
1007 stop_loop = 0
1008 while stop_loop == 0:
1009 try:
1010 year, month, day = [int(x) for x in exp.split('-')]
1011 dt.datetime(year, month, day)
1012 stop_loop = 1
1013 except:
1014 exp = input('Enter expiration date [YYYY-MM-DD]: ')
1015
1016 V = input('Enter option price: ')
1017 stop_loop = 0
1018 while stop_loop == 0:
1019 try:
1020 V = float(V)
1021 stop_loop = 1
1022 except:
1023 V = input('Enter the option price: ')
1024
1025 S = input('Enter the stock price: ')
1026 stop_loop = 0
1027 while stop_loop == 0:
1028 try:
1029 S = float(S)
1030 stop_loop = 1
1031 except:
1032 S = input('Enter the stock price: ')
1033
1034 K = input('Enter the strike price: ')
1035 stop_loop = 0
1036 while stop_loop == 0:
1037 try:
1038 K = float(K)
1039 stop_loop = 1
1040 except:
1041 K = input('Enter the strike price: ')
1042
1043 r = input('Enter the risk free rate: ')
1044 stop_loop = 0
1045 while stop_loop == 0:
1046 try:
1047 r = float(r)
1048 stop_loop = 1
1049 except:
1050 r = input('Enter the risk free rate: ')
1051
1052 # Get date/time
1053 # Same code as in the inputs function (copied-pasted)
1054 # Bad to do this I know
1055 datetime_options = ['0', '1']
1056
1057 print(
1058 '\n"Time" refers to the time to use for the time to expiration calculation.')
1059 print(
1060 'Example: if it is currently the weekend, and you want to see the metrics')
1061 print(
1062 'based on EOD Friday (which is what the prices will be from), enter "1",')
1063 print(
1064 'and enter the date of the most recent Friday, with 16:00 as the time (4pm).\n')
1065 which_datetime_string = 'Enter 0 to use current date/time, 1 to specify date/time: '
1066 which_datetime = input(which_datetime_string)
1067
1068 while which_datetime not in datetime_options:
1069 which_datetime = input(which_datetime_string)
1070
1071 if which_datetime == '0':
1072 now = dt.datetime.now()
1073 current_date = str(now.date())
1074 current_time = '{}:{}'.format(
1075 now.time().hour, now.time().minute)
1076
1077 elif which_datetime == '1':
1078 current_date_string = 'Enter current date [YYYY-MM-DD]: '
1079 current_date = input(current_date_string)
1080
1081 try:
1082 year, month, day = [int(x)
1083 for x in current_date.split('-')]
1084 dt.datetime(year, month, day)
1085
1086 except:
1087 stop_main_loop = 0
1088
1089 while stop_main_loop == 0:
1090 current_date = input(current_date_string)
1091
1092 try:
1093 year, month, day = [int(x)
1094 for x in current_date.split('-')]
1095 dt.datetime(year, month, day)
1096 stop_main_loop = 1
1097
1098 except:
1099 stop_main_loop = 0
1100
1101 current_time_string = 'Enter current 24H time [HH:MM]: '
1102 current_time = input(current_time_string)
1103
1104 try:
1105 hour, minute = [int(x) for x in current_time.split(':')]
1106 dt.datetime(year, month, day, hour, minute)
1107
1108 except:
1109 stop_main_loop = 0
1110
1111 while stop_main_loop == 0:
1112 current_time = input(current_time_string)
1113
1114 try:
1115 hour, minute = [int(x)
1116 for x in current_time.split(':')]
1117 dt.datetime(year, month, day, hour, minute)
1118 stop_main_loop = 1
1119
1120 except:
1121 stop_main_loop = 0
1122
1123 option = Option(current_date, current_time,
1124 opt_type, exp, V, S, K, r, 0, 0, 0, 0)
1125
1126 names = [
1127 'IV',
1128 'delta',
1129 'gamma',
1130 'vega',
1131 'theta',
1132 'rho'
1133 ]
1134
1135 print('\n{}'.format(option))
1136 for name in names:
1137 greek = eval('option.{}'.format(name))
1138 print('{}: {}'.format(name, greek))
1139 print('')
1140
1141 pass
1142
1143
1144if __name__ == '__main__':
1145 main()