Python API Usage Examples

These are the basic usage examples of the Otyca Python API.

  1import pandas as pd
  2import numpy as np
  3import datetime as dt
  4import dateutil.parser
  5import logging
  6from bidict import bidict
  7import atexit
  8from parse import parse
  9
 10from otyca_cpp import otyca_cpp
 11
 12def setup_logging(logger, level=logging.INFO):
 13    """
 14    Sets up a logging callback to bridge C++ logging (from `otyca_cpp`) into Python's logging system.
 15
 16    This function maps C++ log levels defined in `otyca_cpp.LogLevel` to corresponding Python `logging` levels.
 17    It sets a logging callback so that log messages from the C++ side are passed to the given Python logger.
 18
 19    It also ensures that the logging callback is cleared automatically at program exit.
 20
 21    Args:
 22        logger (logging.Logger): The Python logger instance to use for logging messages.
 23        level (int): The minimum Python logging level to capture (e.g., logging.INFO, logging.DEBUG).
 24    """
 25    level_map = bidict()
 26    level_map[otyca_cpp.LogLevel.Debug] = logging.DEBUG
 27    level_map[otyca_cpp.LogLevel.Info] = logging.INFO
 28    level_map[otyca_cpp.LogLevel.Warning] = logging.WARNING
 29    level_map[otyca_cpp.LogLevel.Error] = logging.ERROR
 30
 31    def python_logging_callback(level, message):
 32        # Use the mapped level and log the message
 33        python_level = level_map.get(level, logging.INFO)  # Default to INFO if level not found
 34        logger.log(python_level, message)
 35
 36    otyca_cpp.Logger.set_log_callback(python_logging_callback)
 37    otyca_cpp.Logger.set_log_level(level_map.inverse[level])
 38
 39    def clear_logging_callback():
 40        otyca_cpp.Logger.set_log_callback(None)
 41
 42    atexit.register(clear_logging_callback)
 43
 44def try_float(value, default=np.nan):
 45    try:
 46        return float(value)
 47    except:
 48        return default
 49
 50logger = logging.getLogger(__name__)
 51
 52setup_logging(logger, logging.ERROR)
 53
 54quotedata_file = open('spy_quotedata.csv', "r")
 55lines = quotedata_file.read().splitlines()
 56
 57# create UnderlyingDataset
 58uds = otyca_cpp.UnderlyingDataset()
 59
 60underlying = uds.create_underlying()
 61
 62r = parse('"Date: {valuation_time}",Bid: {bid},Ask: {ask},Size: {bid_size}*{ask_size},{volume}', lines[2])
 63uds.set_valuation_time(dateutil.parser.parse(r['valuation_time']))
 64underlying.set_bid_ask(float(r['bid']), int(r['bid_size']), float(r['ask']), int(r['ask_size']))
 65
 66# overwrite if needed
 67# uds.set_option_style(otyca_cpp.OptionStyle.AMERICAN)
 68# uds.set_option_settlement_lag(1)
 69# uds.set_underlying_settlement_lag(1)
 70# uds.set_exercise_settlement_lag(1)
 71
 72# now create options
 73for option_line in lines[4:]:
 74    option_line = option_line.split(',')
 75
 76    expiration = dt.datetime.strptime(option_line[0], '%a %b %d %Y').replace(hour=16)
 77    strike = float(option_line[11])
 78
 79    bid = try_float(option_line[4])
 80    ask = try_float(option_line[5])
 81    opt = uds.create_option(expiration, strike, otyca_cpp.OptionType.CALL)
 82    opt.set_bid_ask(bid, 1, ask, 1)  # the example data doesn't have sizes
 83
 84    bid = try_float(option_line[15])
 85    ask = try_float(option_line[16])
 86    opt = uds.create_option(expiration, strike, otyca_cpp.OptionType.PUT)
 87    opt.set_bid_ask(bid, 1, ask, 1)  # the example data doesn't have sizes
 88
 89# add projected dividend, assuming annualized yield of 1.2%, fade from pure cash to pure proportional in 1.5 years
 90uds.add_projected_dividend(dt.date(2025, 3, 21), 1.6932, 0, 0)
 91uds.add_projected_dividend(dt.date(2025, 6, 20), 1.4110, 0.0005, 0)
 92uds.add_projected_dividend(dt.date(2025, 9, 19), 1.1288, 0.0010, 0)
 93uds.add_projected_dividend(dt.date(2025, 12, 19), 0.8466, 0.0015, 0)
 94uds.add_projected_dividend(dt.date(2026, 3, 20), 0.5644, 0.0020, 0)
 95uds.add_projected_dividend(dt.date(2026, 6, 19), 0.2822, 0.0025, 0)
 96uds.add_projected_dividend(dt.date(2026, 9, 18), 0, 0.003, 0)
 97uds.add_projected_dividend(dt.date(2026, 12, 18), 0, 0.003, 0)
 98uds.add_projected_dividend(dt.date(2027, 3, 19), 0, 0.003, 0)
 99uds.add_projected_dividend(dt.date(2027, 6, 18), 0, 0.003, 0)
100uds.add_projected_dividend(dt.date(2027, 9, 17), 0, 0.003, 0)
101uds.add_projected_dividend(dt.date(2027, 12, 17), 0, 0.003, 0)
102
103# now create yield curve (risk free discount curve)
104yc = otyca_cpp.PieceWiseYieldCurve()
105yc.add(0.219, otyca_cpp.Estimate(0.04544, 0.005))
106yc.add(0.526, otyca_cpp.Estimate(0.04424, 0.004))
107yc.add(1.025, otyca_cpp.Estimate(0.04289, 0.003))
108yc.add(2.770, otyca_cpp.Estimate(0.04109, 0.003))
109uds.set_yield_curve(yc)
110
111# now create borrow curve, assuming 0 borrow with 5% uncertainty
112bc = otyca_cpp.PieceWiseYieldCurve()
113bc.add(1, otyca_cpp.Estimate(0, 0.05))
114bc.add(2, otyca_cpp.Estimate(0, 0.05))
115uds.set_borrow_curve(bc)
116
117# overwrite if needed
118# tc = otyca_cpp.RealizedVarianceTenorCalculator()
119# tc.set_variance_weights(0.63, 0.09, 0.28)
120# uds.set_variance_tenor_calculator(tc)
121
122
123# add a stochastic jump diffusion model which consists of Heston diffusion (LCIR) and Merton (MT) jump.
124# add upto 3 Bimodal Gaussian jumps and calibrate the process using all options with tenor in the range of
125# (0, 2] years
126uds.add_composite_stochastic_process_config(otyca_cpp.CompositeStochasticProcessConfig("LCIR(MT)",3, 0, 2))
127
128# add a stochastic jump diffusion model which consists of Heston diffusion (LCIR) and double exponential (KOU)
129# jump. add upto 1 Bimodal Gaussian jump and calibrate the process using all options with tenor in the range of
130# (0, 0.5] years
131uds.add_composite_stochastic_process_config(otyca_cpp.CompositeStochasticProcessConfig("LCIR(KOU)", 1, 0, 0.5))
132
133# after all the inputs are setup, call setup()
134uds.setup()
135
136# now call various getters to retrieve calculated results
137
138# get the 3 month and 1 year atm volatility evaluated by each calibrated stochastic jump diffusion model
139for p in uds.get_composite_stochastic_processes():
140    print(f'{p.name()} 3 month atm vol: {p.volatility(0.25, 0)}')
141    print(f'{p.name()} 1 year atm vol: {p.volatility(1, 0)}')
142
143# get implied event schedule
144print(uds.get_implied_event_schedule())
145
146# get dividend schedule
147print(uds.get_dividend_schedule())
148
149# get expiration by expiration values
150print("forward_value,forward_uncertainty,implied_interest_value,implied_interest_uncertainty,"
151      "implied_borrow_value,implied_borrow_uncertainty,implied_forward_value,implied_forward_uncertainty,"
152      "variance_term_structure_model_value,fair_atm_volatility,atm_volatility_value,"
153      "fair_volatility_skew,vix_style_volatility,atm_volatility_uncertainty")
154for option_expiration in uds.get_option_expirations():
155    print(f"{option_expiration.get_forward_value()},"
156          f"{option_expiration.get_forward_uncertainty()},"
157          f"{option_expiration.get_implied_interest_value()},"
158          f"{option_expiration.get_implied_interest_uncertainty()},"
159          f"{option_expiration.get_implied_borrow_value()},"
160          f"{option_expiration.get_implied_borrow_uncertainty()},"
161          f"{option_expiration.get_implied_forward_value()},"
162          f"{option_expiration.get_implied_forward_uncertainty()},"
163          f"{option_expiration.get_variance_term_structure_model_value()},"
164          f"{option_expiration.get_fair_atm_volatility()},"
165          f"{option_expiration.get_atm_volatility_value()},"
166          f"{option_expiration.get_fair_volatility_skew()},"
167          f"{option_expiration.get_vix_style_volatility()},"
168          f"{option_expiration.get_atm_volatility_uncertainty()}")
169
170# get strike by strike values
171print("expiration,strike,call_bid_volatility,call_ask_volatility,put_bid_volatility,put_ask_volatility,"
172      "strike_volatility_value,strike_volatility_uncertainty,fair_volatility,")
173for option_expiration in uds.get_option_expirations():
174    for option_strike in option_expiration.get_option_strikes():
175        call = option_strike.get_call()
176        put = option_strike.get_put()
177        print(
178            f"{option_expiration.get_expiration().date()},"
179            f"{option_strike.get_strike()}",
180            f"{call.get_bid_volatility()},"
181            f"{call.get_bid_volatility()},"
182            f"{put.get_bid_volatility()},"
183            f"{put.get_bid_volatility()},"
184            f"{option_strike.get_strike_volatility_value()},"
185            f"{option_strike.get_fair_volatility()},"
186            f"{option_strike.get_strike_volatility_uncertainty()},")
187
188# dataframe style
189ticker = 'SPY'
190data = uds.get_implied_interest_borrow_dataframe()
191implied_interest_borrow_df = pd.DataFrame(data=np.array(data[2]).transpose(), columns=data[1],
192                                          index=pd.MultiIndex.from_tuples([(ticker, m) for m in data[0]],
193                                                                          names=['tkr', 'exp']))
194print(implied_interest_borrow_df.head())
195
196data = uds.get_expiration_volatility_model_dataframe()
197exp_vol_model_df = pd.DataFrame(data=np.array(data[2]).transpose(), columns=data[1],
198                                index=pd.MultiIndex.from_tuples([(ticker, m) for m in data[0]], names=['tkr', 'exp']))
199print(exp_vol_model_df.head())
200
201data = uds.get_option_expiration_dataframe()
202expiration_df = pd.DataFrame(data=np.array(data[2]).transpose(),
203                             index=pd.MultiIndex.from_tuples([(ticker, m) for m in data[0]], names=['tkr', 'exp']),
204                             columns=data[1])
205print(expiration_df.head())
206
207data = uds.get_option_pricing_dataframe()
208option_pricing_df = pd.DataFrame(data=np.array(data[2]).transpose(),
209                                 index=pd.Index([f'{ticker}{o}' for o in data[0]], name='opt_tkr'),
210                                 columns=data[1])
211print(option_pricing_df.head())
212
213data = uds.get_option_bid_ask_implied_volatility_dataframe()
214option_bid_ask_df = pd.DataFrame(data=np.array(data[2]).transpose(),
215                                 index=pd.Index([f'{ticker}{o}' for o in data[0]], name='opt_tkr'),
216                                 columns=data[1])
217print(option_bid_ask_df.head())
218
219for option_expiration in uds.get_option_expirations():
220    data = option_expiration.get_option_strike_dataframe()
221    option_strike_df = pd.DataFrame(data=np.array(data[2]).transpose(),
222                                    index=pd.Index(data[0], name='k'),
223                                    columns=data[1])
224    option_strike_df['exp'] = option_expiration.get_expiration()
225    print(option_strike_df.head())
Preview of spy_quotedata.csv

SPDR S&P 500 ETF Trust,Last: 564.4,Change:  3.38
"Date: March 19, 2025 at 10:57 AM EDT",Bid: 564.39,Ask: 564.4,Size: 1*1,"Volume: 6,805,714"
Expiration Date,Calls,Last Sale,Net,Bid,Ask,Volume,IV,Delta,Gamma,Open Interest,Strike,Puts,Last Sale,Net,Bid,Ask,Volume,IV,Delta,Gamma,Open Interest
Wed Mar 19 2025,SPY250319C00400000,161.33,0.795,163.15,165,4,5.5771,0.9997,0,132,400.00,SPY250319P00400000,0.01,0,0,0.01,0,4.003,-0.0003,0,752
Wed Mar 19 2025,SPY250319C00405000,153.27,0,158.16,160.9,0,5.322,0.9997,0,8,405.00,SPY250319P00405000,0.01,0.005,0,0.01,2,3.8669,-0.0003,0,5
Wed Mar 19 2025,SPY250319C00410000,147.83,0,153.18,155.86,0,5.8506,0.9997,0,8,410.00,SPY250319P00410000,0.01,0,0,0.01,0,3.7323,-0.0003,0,39
Wed Mar 19 2025,SPY250319C00415000,0,0,148.17,150.81,0,5.9167,0.9997,0,0,415.00,SPY250319P00415000,0.01,0,0,0.01,0,3.5992,-0.0003,0,7
Wed Mar 19 2025,SPY250319C00420000,139.34,0,143.2,146.12,0,5.0644,0.9996,0,4,420.00,SPY250319P00420000,0.02,0,0,0.01,0,3.4674,-0.0004,0,13225
Wed Mar 19 2025,SPY250319C00425000,0,0,138.17,140.93,0,0,0.9996,0,0,425.00,SPY250319P00425000,0.01,0,0,0.01,0,3.3368,-0.0004,0,2256

Download full CSV