diff --git a/.decommissioned/WFA.py b/.decommissioned/WFA.py deleted file mode 100644 index 8e2cd8d..0000000 --- a/.decommissioned/WFA.py +++ /dev/null @@ -1,372 +0,0 @@ -import os -import sys -import pickle -from backtesting.backtesting import Strategy -import pandas as pd -from pandas.tseries.offsets import BDay -import numpy as np -from datetime import datetime -from trade.backtester_.utils.WalkForwardUtils import * -from trade.backtester_.Universe import universe -from trade.helpers.helper import printmd -UNIVERSE = universe - - - -class WFO: - is_train_backtest = True # TO ONLY - is_test_backtest = True - is_reopened = False - prev_open_positions_dict = None - is_position_open = False - open_positions = [] - counter = 50 - official_start = None - i = 0 - size = None - exit_price = None - exit_date = None - init_buy = None - log = pd.DataFrame() - should_log = False - - - - def init(self): - super().init() - self.last_date = self.data.index[-1] - if self.prev_open_positions_dict and len(self.prev_open_positions_dict) != 0: - if self._name in self.prev_open_positions_dict.keys(): - position_details = self.prev_open_positions_dict[self._name] - self.size, self.exit_price, self.exit_date = position_details['Size'], ((position_details['ExitPrice'])), pd.to_datetime(position_details['ExitTime'] - BDay(0)) - # Using prev day because backtesting.py buys on next day - assert (self.is_train_backtest or self.is_test_backtest) and not (self.is_train_backtest and self.is_test_backtest), "Exactly one of self.is_train_backtest or self.is_test_backtest must be True." - assert self.official_start, "Test/Validation backtest needs self.official_start to run." - - def next(self): - # ## REOPEN PREV OPEN POSITIONS - - date = self.data.index[-1] ## SET CURRENT DATE ON EACH NEXT LOOP - if self.is_train_backtest: - self.train_backtest(date) ## Begin train_backtest if user wants to train (setting is_train_backtest to true) - elif self.is_test_backtest: - - ## Begin test_backtest if user wants to test (setting is_test_backtest to true) - self.test_backtest(date) - - def check_open_positions(self, date): # This creates a dictionary holding positions that weren't closed. This is necessary for positions to be carried over to next validation run - # This is only created in test backtest because we are only accumulating returns of validation runs - - if date == self.last_date : - # print('HI', self.last_date, 'Test run', self.is_test_backtest, 'Train run', self.is_train_backtest) - pass - # if self.position: - # self.open_positions.append({'Name': self._name,'Size' : self.position.size, 'ExitDate': self.last_date, 'Close': self.data.Close[-1], 'Prev_Close': self.data.Close[-2]}) - # else: - # self.open_positions.append({'Name': self._name, 'Size' : 'NO OPEN POSITION', 'ExitDate': self.last_date, 'Close': self.data.Close[-1], 'Prev_Close': self.data.Close[-2]}) - - - # self.logger({'Date': [date], - # 'Close': [self.data.Close[-1]], - # 'Upper Band': [self.upper_inner_band[-1]], - # 'Name': [self._name], - # 'Crossover Flag': [crossover(self.data.Close, self.upper_inner_band)] - # }) if self.should_log else None - - - def train_backtest(self, date): - - self.prev_open_positions_dict = None # Handle reset of this variable incase it hasn't already been handled - if date >= self.official_start: - super().next() - self.check_open_positions(date) #Comment this out when officially carrying out WFO - - def test_backtest(self, date): - assert self.prev_open_positions_dict is not None, f"self.prev_open_positions_dict cannot be none in test_backtest. If there are no previous positions, pass an empty dictionary" - - # if self._name in self.prev_open_positions_dict and not self.is_reopened: - # self.reopen_previous_position(date) - # else: - if date >= self.official_start: - super().next() - self.check_open_positions(date) - - def reopen_previous_position(self, date): - if date >= self.exit_date and not self.position and not self.is_reopened: - self.init_buy = date - self.buy() - self.is_reopened = True - print(f'Reopened {self._name} on {date}, for size {self.size}') - - - @classmethod - def logger(cls, dictionary): - item = pd.DataFrame(dictionary) - cls.log = pd.concat([cls.log, item]).reset_index(drop =True) - - - @classmethod - def reset_variables(cls): - # PTBacktester is basically looping individual backtest.run(). Backtest object USES a deep copy of strategy object, which means every instance of Backtest will have the same - # strategy object & variables. Therefore to ensure that during the PTBacktester looping has fresh values, we have to reset position related variables. - - cls.exit_price = None - cls.exit_date = None - cls.size = None - cls.is_reopened = False - cls.is_train_backtest = True - cls.is_test_backtest = True - - - - - - -from typing import Type, Union, Tuple -from backtesting.backtesting import Strategy -from datetime import datetime -import asyncio -class WalkForwardAnalysis: - def __init__(self, names: list, strategy: Union[Type, Type[Strategy]], optimize_var: dict, engine: str): - assert engine in ['position', 'cross'], f'Available engines are "position" or "cross". Recieved "{engine}"' - self.names = names - self.strategy = strategy - self.split_datasets = None # Initiated by method split_dataset - self.settings = None # Settings holding other important attributes. This just helps us ensure we can expand the code w/o over populating the __init__. - # This is set by the set_settings method, will be called at initialization since there are defaults - - self.strategy_settings_lib = None - self.optimize_var = optimize_var - self.windows = None # Dict holding the windows for everthing which includes: - # Train: Start, end - # Test: Official Start, Start, end. - # This is set by a method split_windows. Can't split at initiation cause we need to pass the settings first - self.trainOpt_data = None # Variable holding the data obtained from running an optimization on train dataset - self.tested_data = None # Variable holding the data obtained from OOS testing - self.engine = engine #String with name - self.set_settings({}) - - # async def run(self): - # variable = await self.run_process() - # if variable: - # self.save_class() - - # return variable - - # async def run_process(self): - def run(self): - train_windows = self.windows['train_window'] - test_windows = self.windows['test_window'] - train_data = self.split_datasets['train_window'] - test_data = self.split_datasets['test_window'] - train_packaged_data = {} - test_packaged_data = {} - to_dataframe_dict = {} - wfe_data = {} - mega_data = {} - saved_strat_settings = {} - val_, train_start, train_end, test_start, test_end, train_CAGR, test_CAGR, wfe_, train_drawdown = [], [], [], [],[], [], [], [],[] - for val_run, data in test_data.items(): - printmd(f"### **Validation Run: {val_run}**") if self.printHeaders else None - ## RETRIEVE RUN DATA & SPLIT INTO VARIABLES - test_run_data = data - train_run_data = train_data[val_run] - off_start = self.windows['test_window'][val_run]['off_start'] - test_end_date = self.windows['test_window'][val_run]['End'] - train_end_date = self.windows['train_window'][val_run]['End'] - off_start_train = self.windows['train_window'][val_run]['off_start'] - no_days_traded_in_test = np.busday_count(off_start.strftime('%Y-%m-%d'),test_end_date) - - ## TRAIN DATA - trained_data = self.train(train_run_data, off_start_train) - trained_target_metric = trained_data['agg']['CAGR [%]'] - - ## TEST WITH STRATEGY SETTING - strategy_setting = trained_data['strategy_settings'] - tested_data = self.test(test_run_data, strategy_setting, off_start) - tested_target_metric = tested_data['agg']['CAGR [%]'] - tested_drawdown = tested_data['agg']['Max. Drawdown [%]'] - annualized_drawdown = tested_drawdown * 1/(no_days_traded_in_test/260) - - ## SAVE AGG FROM BOTH - train_packaged_data[val_run] = trained_data - test_packaged_data[val_run] = tested_data - wfe_data[val_run] = tested_target_metric/trained_target_metric - saved_strat_settings[val_run] = strategy_setting - ## PRINT WFE FOR THE RUN - print(f"Validation Run: {val_run}, WFE: {tested_target_metric/trained_target_metric}, Test CAGR [%]: {tested_target_metric}, Train CAGR [%]: {trained_target_metric}, Annualized Drawdowon [%]: {annualized_drawdown}") if self.printHeaders else None - - ## Appending data to list which goes into dataframe - for lst, val in zip([val_, train_start, train_end, test_start, test_end, train_CAGR, test_CAGR, wfe_, train_drawdown], - [val_run, off_start_train.strftime('%Y-%m-%d'), train_end_date, off_start.strftime('%Y-%m-%d'), test_end_date, trained_target_metric, tested_target_metric,tested_target_metric/trained_target_metric ,tested_drawdown]): - lst.append(val) - ## Creating a data dictionary - - for lst, col in zip([train_start, train_end, test_start, test_end, train_CAGR, test_CAGR, wfe_, train_drawdown], - ['Train_Start_Date', 'Train_end_Date', 'Test_Start_Date', 'Test_End_Date', 'Train_CAGR', 'Test_CAGR', 'WFE', 'TEST_ANNUALIZED_DRAWDOWN']): - to_dataframe_dict[col.upper()] = lst - - ## Saving data to class attributes - self.trained_data = train_packaged_data - self.tested_data = test_packaged_data - self.strategy_settings_lib = saved_strat_settings - self.WFE_data = wfe_data - stats = pd.DataFrame(index = pd.Index(val_, name = 'VALIDATION_RUN', dtype = 'int64'), data = to_dataframe_dict ) - stats['WFE_ADJUSTED'] = stats.WFE * np.sign(stats.TRAIN_CAGR) - self.stats = stats - self.save_class() - - return stats - - def save_class(self): - className = self.strategy.__bases__[1].__name__ - print(className) - anc = 'ANCHORED' if self.anchored else 'UNANCHORED' - name = f'{className}_{"_".join(self.names)}_lookback_{self.lookback_bars}_val_{self.validation_bars}_warmup_{self.warmup_bars}' - today = datetime.today().strftime('%Y-%d-%m') - save_location = f'WFA/{today}/{anc}/{name}.pkl' - dir = os.path.dirname(save_location) - os.makedirs(dir, exist_ok = True) - with open(save_location, 'wb') as file: - pickle.dump(self, file) - - - def split_(self) -> Tuple[dict, dict]: - """ - Returns a dictionary holding both the split up window & corresponding dataset objects to carry out backtesting. - - """ - return split_window(stocks = self.names, - strategy= self.strategy, - data_end= self.data_end, - data_length_str= self.data_length_str, - warmup_bars = self.warmup_bars, - lookback_bars= self.lookback_bars, - validation_bars = self.validation_bars, - anchored = self.anchored, - interval = self.interval - ) - - - def set_settings(self, settings) -> None: - """ - Attribute responsible for initiating the necessary settings to assist with the WFA. Pass a dict with setting name as key and corresponding setting as values - - dict params: - ____________ - - BaseRun (bool): designates whether this WFA is a base Run (no optimization, just runs of split up data with constant parameters) - anchored (bool): True to run an anchored WFA or False to not - printHeaders (bool): Bool deciding whether to print headers or not - data_end (datetime): Datetime object for when the WFA data should end - warmup_bars (int): Number of bars to be used as warmup bars - lookback_bars (int): Number of bars to be used as lookback/train bars - validation_bars (int): Number of bars to be used in valudation - cash (int, dict): Cash value. int defaults to setting all names with the cash value supplied. Dict must be {ticker: cash} with corresponding names in names - commission (float): Commission - data_length_str (string): Length string to evaluate. Eg: years = 6. refer to dateutils.relativedelta.relativedelta for available options - interval (string): Timeseries interval - optimize_str (str): Applicable to 'position' engine in WF. The associated string to be optimized from backtesting.py optimizer - optimize_list (list): Applicable to 'cross' engine in WF. Associated list of items to be optimized in PTBacktester optimizer - target_metric (str): This is the index name as seen in the aggregate function. PLEASE PASS EXACTLY - - Defaults: - __________ - - {'BaseRun': False, 'anchored': False, 'printHeaders': False, 'data_end': datetime.datetime(2024, 8, 16, 16, 58, 5, 875120), 'warmup_bars': 300, 'lookback_bars': 1308, 'cash': 1000, - 'commission': 0.002, 'interval': '1d', 'validation_bars': 126, 'data_length_str': 'years = 15', 'optimize_str': 'Return [%]', 'optimize_list': ['rtrn']} - """ - - settings_list = ['BaseRun', 'anchored', 'printHeaders', 'data_end', 'warmup_bars', 'lookback_bars', 'cash', 'commission', 'interval', 'validation_bars', 'data_length_str', 'optimize_str', 'optimize_list', 'target_metric'] # List of available settings - settings_default = [False, False, False, datetime.today(), 300, 252*4+300, 1000, 0.002, '1d', 126, 'years = 15', 'Return [%]', ['rtrn'], 'Return [%]'] # Available settings corresponding default args - settings_type = [bool, bool, bool, datetime, int, int, [int, dict, float], float, str, int, str, str, list, str] # Available settings corresponding datatype - settings_default_dict = dict(zip(settings_list, settings_default)) #Creating settings default - settings_criteria = dict(zip(settings_list, settings_type)) # Creating dict with settings type to assert types allowed - - for i, (key, value) in enumerate(settings.items()): - assert key in settings_list, f"Setting '{key}' not a valid settings. Valid settings: {settings_list}" - if key == 'cash': - assert isinstance(value, settings_criteria[key][0]) or isinstance(value, settings_criteria[key][1]) or isinstance(value, settings_criteria[key][2]), f"Type {type(value)} not a valid type for '{key}', expecting {settings_criteria[key]}" - else: - assert isinstance(value, settings_criteria[key]), f"Type {type(value)} not a valid value for {key}, expecting {settings_criteria[key]}" - settings_default_dict[key] = value - - for key, value in settings_default_dict.items(): - setattr(self, key, value) - self.settings = settings_default_dict - self.windows, self.split_datasets = self.split_() - - def train(self, data: list, off_start: pd.Timestamp): - - if self.engine == 'position': - agg_train = position_train(self.strategy, data, self.optimize_var, off_start =off_start, optimize_str= self.optimize_str, cash = self.cash, baseRun = self.BaseRun, printHeaders = self.printHeaders ) - else: - agg_train = cross_train(self.strategy, data, self.optimize_var,off_start =off_start, optimize_list= self.optimize_list, cash = self.cash,baseRun = False, printHeaders = self.printHeaders) - return agg_train - - def test(self, data: list, strategy_setting: dict, off_start: pd.Timestamp): - if self.engine == 'position': - agg_test = position_test(strategy = self.strategy, off_start= off_start, strategy_settings= strategy_setting,validation_datas= data,cash = self.cash, commission= self.commission, plot_positions= False ) - else: - agg_test = cross_test(self.strategy, off_start, strategy_setting, data,self.cash, self.commission) - return agg_test - - - - - - def produce_summary(self, data_choice: str = 'test') -> pd.Series: - """ - Params: - ________ - - data_choice: 'test' to recieve summary for OOS data and 'train' for IS data - - - Returns: - _________ - pd.Series - - """ - assert self.tested_data is not None, f"Please run Walk Forward Analysis to produce necessary datapoints" - assert data_choice in ['test', 'train'], f"Only options for summary production is 'test' and 'train'. Recieved '{data_choice}'" - def compute_summary(data_choice): - dataChoice = self.tested_data if data_choice == 'test' else self.trained_data - val_run = list(dataChoice.keys()) - metrics = ['# Trades', 'Best Trade [%]', 'Worst Trade [%]', 'Avg. Winning Trade [%]', 'Avg. Losing Trade [%]', 'Avg. Trade [%]', 'Winning Streak', 'Losing Streak'] - windows = self.windows['train_window'] if data_choice == 'train' else self.windows['test_window'] - summary = pd.DataFrame(index = val_run) - for col in metrics: - for run in val_run: - no_bus_days = np.busday_count(windows[run]['off_start'].strftime('%Y-%m-%d'), windows[run]['End']) - summary.at[run, col] = dataChoice[run]['agg'][col] - summary.at[run, 'Net PnL'] = dataChoice[run]['agg']['Equity Final [$]'] - dataChoice[run]['agg']['equity_curve']['Total'][0] - summary.at[run, 'Annualized Net PnL'] = annualize_net_profit(dataChoice[run]['agg']['Equity Final [$]'] - dataChoice[run]['agg']['equity_curve']['Total'][0],dataChoice[run]['agg']['equity_curve']['Total'][0], no_bus_days) - summary.at[run, 'Annualized Net PnL [%]'] = annualize_net_profit(dataChoice[run]['agg']['Equity Final [$]'] - dataChoice[run]['agg']['equity_curve']['Total'][0],dataChoice[run]['agg']['equity_curve']['Total'][0], no_bus_days,False) - summary.at[run, 'Net PnL [%]'] = (dataChoice[run]['agg']['Equity Final [$]'] - dataChoice[run]['agg']['equity_curve']['Total'][0])/dataChoice[run]['agg']['equity_curve']['Total'][0] - summary.at[run, 'Losing Trades'] = (dataChoice[run]['agg']['# Trades'] * (dataChoice[run]['agg']['Lose Rate [%]']/100)).round(0) - summary.at[run, 'Winning Trades'] = (dataChoice[run]['agg']['# Trades'] * (1-(dataChoice[run]['agg']['Lose Rate [%]']/100))).round(0) - summarized_summary = pd.Series() - summarized_summary['Avg Net Profit'] = summary['Net PnL'].mean() - summarized_summary['Avg Annualized Net Profit'] = summary['Annualized Net PnL'].mean() - summarized_summary['Annualized Net PnL [%]'] = summary['Annualized Net PnL [%]' ].mean() - summarized_summary['Avg Net Profit'] = summary['Net PnL'].mean() - summarized_summary['Avg Net Profit [%]'] = summary['Net PnL [%]'].mean() - summarized_summary['Total # of Trades'] = summary['# Trades'].sum() - summarized_summary['Total # of Winning Trades'] = summary['Winning Trades'].sum() - summarized_summary['Total # of Losing Trades'] = summary['Losing Trades'].sum() - summarized_summary['Largest Losing Trades [%]'] = summary['Worst Trade [%]'].min() - summarized_summary['Largest Winning Trades [%]'] = summary['Best Trade [%]'].max() - summarized_summary['Avg. Winning Trade [%]'] = summary['Avg. Winning Trade [%]'].mean() - summarized_summary['Avg. Losing Trade [%]'] = summary['Avg. Losing Trade [%]'].mean() - summarized_summary['Avg. Trade [%]'] = summary['Avg. Trade [%]'].mean() - summarized_summary['Max Winning Streak'] = summary['Winning Streak'].max() - summarized_summary['Max Losing Streak'] = summary['Losing Streak'].max() - return summarized_summary - - test_summary = compute_summary('test') - train_summary = compute_summary('train') - test_summary['WFE'] = test_summary['Avg Annualized Net Profit']/train_summary['Avg Annualized Net Profit'] - train_summary['WFE'] = test_summary['Avg Annualized Net Profit']/train_summary['Avg Annualized Net Profit'] - - return test_summary if data_choice == 'test' else train_summary diff --git a/.decommissioned/WalkForwardUtils.py b/.decommissioned/WalkForwardUtils.py deleted file mode 100644 index 0777959..0000000 --- a/.decommissioned/WalkForwardUtils.py +++ /dev/null @@ -1,328 +0,0 @@ -import os -import sys -from trade.backtester_.backtester_ import PTBacktester, PTDataset -from trade.assets.Stock import Stock -import pandas as pd -from pandas.tseries.offsets import BDay -from datetime import datetime -import yfinance as yf -from trade.backtester_.Universe import universe -UNIVERSE = universe - - - -def create_datasate(stocks: list, start: str,interval: str, engine: str = 'yf', timewidth = None, timeframe = None, end: str = datetime.today(), return_object = False ): - dataset = [] - raw_dataset = {} - data_range = pd.date_range(start, end, freq = 'B') - if engine.lower() == 'yf': - from datetime import datetime - for stock in stocks: - data2 = yf.download(stock, start = start, end = end, interval=interval, progress = False) - if pd.isna(data2.Open.max()): - pass - else: - raw_dataset[stock] = data2 - dataset.append(PTDataset(stock, data2)) - else: - for stk in stocks: - stock = Stock(stk) - data = stock.spot(ts = True, ts_start = '2018-01-01', ts_timeframe=tmframe) - data.rename(columns = {x:x.capitalize() for x in data.columns}, inplace= True) - data['Timestamp'] = pd.to_datetime(data['Timestamp'], format = '%Y-%m-%d') - data2 = data.set_index('Timestamp') - data2 = data2.asfreq('W', method = 'ffill') - data2 = data2.fillna(0) - data2['Next_Day_Open'] = data2.Open.shift(-1) - data2['EMA'] = ta.ma('ema', data2.Close, length = 21).fillna(0) - dataset.append(PTDataset(stk, data2)) - raw_dataset[stock] = data2 - return dataset if return_object else raw_dataset - -def prev_monday(date): - date = pd.to_datetime(date) - day_of_week_ = date.day_of_week - date = date.replace(day = date.day - day_of_week_) - return date - - - - - - -def annualize_net_profit(net_profit, initial_investment, days, value = True): - annualized_profit = ((1 + net_profit / initial_investment) ** (260 / days)) - 1 - return annualized_profit * initial_investment if value else annualized_profit *100 - - - -def split_window( - stocks, - strategy, - data_end, - data_length_str, - warmup_bars, - lookback_bars=28*1440, - validation_bars=7*1440, - anchored = False, - interval = '1d'): - - validation_run = 1 - data_start = eval(f'datetime.today()-relativedelta({data_length_str})') - tester = ['^GSPC'] - data_bank = create_datasate(tester, data_start, interval,end = data_end , return_object=False) - data_dict = create_datasate(stocks, data_start, interval,end = data_end , return_object=False) - data_full = data_bank[tester[0]] - split_data = {} - split_window = {} - train_window = {} - test_window = {} - test_data = {} - train_data = {} - for i in range(lookback_bars+warmup_bars, len(data_full)-validation_bars, validation_bars): - s = i -lookback_bars - warmup_bars if not anchored else 0 - length_filter = 300 - sample_datas = [] - validation_datas = [] - - pass_val_names = [] - ## I NEED TO CREATE A LIST OF SAMPLE DATA PTDATASET - for name, data in data_bank.items(): - off_start_train = pd.to_datetime(data_full.iloc[s+warmup_bars].name) - start = pd.to_datetime(data_full.iloc[s].name).strftime('%Y-%m-%d') - end = pd.to_datetime(data_full.iloc[i].name ).strftime('%Y-%m-%d') - - for stk in stocks: - temp = data_dict[stk] - temp = temp[(temp.index >= start) & (temp.index <= end)] - # if len(temp) = valStart) & (temp.index <= valEnd)] - validation_datas.append(PTDataset(stk, temp)) - - train_window[str(validation_run)] = {'off_start': off_start_train, 'Start': start, 'End': end} - test_window[str(validation_run)] = {'off_start': off_start,'Start': valStart, 'End': valEnd} - train_data[str(validation_run)] = sample_datas - test_data[str(validation_run)] = validation_datas - validation_run += 1 - - split_window['train_window'] = train_window - split_window['test_window'] = test_window - split_data['train_window'] = train_data - split_data['test_window'] = test_data - return split_window, split_data - - -def position_train( - strategy, - sample_datas, - optimize_params, - off_start, - optimize_str = 'Return [%]', - cash=1_000, - commission=0.002, - baseRun = True, - printHeaders = False): - - packaged_data = {} - # Carry out training - strategy.prev_open_positions_dict = None - strategy.official_start = off_start - strategy.open_positions = [] - strategy.is_test_backtest = False - strategy.is_train_backtest = True - bt_training = PTBacktester(sample_datas, strategy, cash=cash, commission=commission) - stats = bt_training.run() - agg1 = bt_training.aggregate() - print('Pre optimize CAGR: ', agg1['CAGR [%]'], ' Return', agg1['Return [%]']) if printHeaders else None - if not baseRun: - # Optimize training data & pick best parameters - - optimized = bt_training.position_optimize(optimize_params, maximize = optimize_str) - strategy_settings = optimized.to_dict('index') - bt_training = PTBacktester(sample_datas, strategy, cash=cash, commission=commission, strategy_settings= strategy_settings) - bt_training.run() - - packaged_data['strategy_settings'] = strategy_settings - packaged_data['agg'] = bt_training.aggregate() - packaged_data['stats_by_tick'] = stats - print('Post optimize CAGR: ', packaged_data['agg']['CAGR [%]'], ' Return', packaged_data['agg']['Return [%]']) if printHeaders else None - - strategy.reset_variables() - return packaged_data - -def cross_train( - strategy, - sample_datas, - optimize_params, - off_start, - optimize_list = ['rtrn'], - cash=1_000, - commission=0.002, - baseRun = True, - printHeaders = False): - - packaged_data = {} - # Carry out training - strategy.prev_open_positions_dict = None - strategy.official_start = off_start - strategy.open_positions = [] - param_dict = {} - strategy.is_test_backtest = False - strategy.is_train_backtest = True - bt_training = PTBacktester(sample_datas, strategy, cash=cash, commission=commission) - stats = bt_training.run() - agg1 = bt_training.aggregate() - print('Pre optimize CAGR: ', agg1['CAGR [%]'], ' Return', agg1['Return [%]']) if printHeaders else None - if not baseRun: - - optimized = bt_training.optimize(optimize_params, optimize_list) - optimized.sort_values(optimize_list[0], ascending = False, inplace = True) - optimized_page = optimized.head(1) - for attr in optimize_params.keys(): - param_dict[attr] = optimized_page[attr].values[0] - - - packaged_data['agg'] = bt_training.aggregate() - packaged_data['stats_by_tick'] = stats - packaged_data['strategy_settings'] = param_dict - print('Post optimize CAGR: ', packaged_data['agg']['CAGR [%]'], ' Return', packaged_data['agg']['Return [%]']) if printHeaders else None - - strategy.reset_variables() - return packaged_data - - - -def position_test( - strategy, - off_start, - strategy_settings, - validation_datas, - cash, - commission, - baseRun = False, - anchored = False, - plot_positions = False): - - """ - This function carries out a test run with per position method. - - Parameters: - _____________ - - strategy: backtesting.backtesting.Strategy object - off_start (pd.Timestamp): Official Start date. This is assuming WFO would be a class in this strategy - strategy_settings (dict): Params as a dict to optimize by. In per ticker format - validation_data (List[PTDataset]): list of PTDataset - cash (int, float, dict): Cash - commission(float): Commission - baseRun (bool): This is assuming there will be no optimizing if True - anchored (bool): True to not move validation period forward, false to move forward - plot_position (bool): True to plot each positions charts - - - Returns: - __________ - - dict: - agg: Aggregate data from PTBacktester - stats_by_tick: Stats by Ticker from backtesting.py - - - """ - - #Run Validation backtest - packaged_data = {} - strategy.official_start =off_start - strategy.is_train_backtest = False - strategy.open_positions = [] - strategy.prev_open_positions_dict = {} # pre_open_positions_dict has be an empty dict in the first run because there are no previous positions - if baseRun: - bt_validation = PTBacktester(validation_datas, strategy, cash=cash, commission=commission) - else: - bt_validation = PTBacktester(validation_datas, strategy, cash=cash, commission=commission, strategy_settings = strategy_settings) - stats_validation = bt_validation.run() - agg_validation = bt_validation.aggregate() - packaged_data['agg'] = agg_validation - packaged_data['stats_by_tick'] = stats_validation - #Plot each validation run - if plot_positions: - for d in bt_validation.datasets: - name = d.name - bt_validation.plot_position(name, filename = f"No Optimization-{off_start.strftime('%Y-%m-%d')}_{name}") - - return packaged_data - - -def cross_test( - strategy, - off_start, - strategy_settings, - validation_datas, - cash, - commission, - baseRun = False, - anchored = False, - plot_positions = False): - - - """ - This function carries out a across positions without changing params per ticker. - - Parameters: - _____________ - - strategy: backtesting.backtesting.Strategy object - off_start (pd.Timestamp): Official Start date. This is assuming WFO would be a class in this strategy - strategy_settings (dict): Params as a dict to optimize by. In per params format - validation_data (List[PTDataset]): list of PTDataset - cash (int, float, dict): Cash - commission(float): Commission - baseRun (bool): This is assuming there will be no optimizing if True - anchored (bool): True to not move validation period forward, false to move forward - plot_position (bool): True to plot each positions charts - - - Returns: - __________ - - dict: - agg: Aggregate data from PTBacktester - stats_by_tick: Stats by Ticker from backtesting.py - - - """ - - - #Run Validation backtest - packaged_data = {} - strategy.official_start =off_start - strategy.is_train_backtest = False - strategy.open_positions = [] - strategy.prev_open_positions_dict = {} #pre_open_positions_dict has be an empty dict in the first run because there are no previous positions - if baseRun: - bt_validation = PTBacktester(validation_datas, strategy, cash=cash, commission=commission) - else: - for attr, value in strategy_settings.items(): - setattr(strategy, attr, value) - bt_validation = PTBacktester(validation_datas, strategy, cash=cash, commission=commission) - stats_validation = bt_validation.run() - agg_validation = bt_validation.aggregate() - packaged_data['agg'] = agg_validation - packaged_data['stats_by_tick'] = stats_validation - #Plot each validation run - if plot_positions: - for d in bt_validation.datasets: - name = d.name - bt_validation.plot_position(name, filename = f"No Optimization-{off_start.strftime('%Y-%m-%d')}_{name}") - - return packaged_data \ No newline at end of file diff --git a/.decommissioned/algo/algo.strategies._utils.py b/.decommissioned/algo/algo.strategies._utils.py deleted file mode 100644 index c902b0d..0000000 --- a/.decommissioned/algo/algo.strategies._utils.py +++ /dev/null @@ -1,391 +0,0 @@ -raise DeprecationWarning("This module is deprecated. Use algo.strategies.utils instead.") -# import os -# import yaml -# import importlib -# from dateutil.relativedelta import relativedelta -# from pathlib import Path -# from datetime import datetime, timedelta -# import pandas as pd -# from dbase.database.SQLHelpers import ( -# get_engine, -# list_tables_from_db, -# create_table_from_schema, -# dynamic_batch_update, -# DatabaseAdapter -# ) -# from dbase.DataAPI.ThetaData import ( -# retrieve_quote, -# retrieve_quote_rt -# ) -# from trade.assets.Calculate import Calculate -# from trade.helpers.helper import ( -# parse_option_tick, -# binomial_implied_vol, -# CustomCache -# ) -# from EventDriven.riskmanager.market_data import ( -# OPTION_TIMESERIES_START_DATE, -# MarketTimeseries -# ) -# from EventDriven.riskmanager.utils import parse_position_id -# from algo.strategies.enums import Action - - -# ## 1). Setup data source. CustomCache location -# DATA_LOCATION = Path(f"{os.environ['GEN_CACHE_PATH']}") -# CUSTOM_CACHE = None - -# ## 2). Object for live data. Reloads after 3 mins -# LIVE_TIMESERIES = None -# def get_live_timeseries_obj() -> MarketTimeseries: -# """ -# Get a MarketTimeseries object that refreshes data every 3 minutes. -# """ -# global LIVE_TIMESERIES - -# ## Start is 2 weeks from today -# end = datetime.now() -# start = end - relativedelta(weeks=2) -# if LIVE_TIMESERIES is None: -# LIVE_TIMESERIES = MarketTimeseries(_refresh_delta=timedelta(minutes=3), -# _start=start.strftime('%Y-%m-%d'), -# _end=end.strftime('%Y-%m-%d')) - -# ## No need for refresh here. It is handled in the class during at_index call -# return LIVE_TIMESERIES - -# def get_custom_cache(loc=DATA_LOCATION): -# """ -# Get a CustomCache instance for storing trading data. -# This function initializes a CustomCache with a specified location and settings. -# """ -# global CUSTOM_CACHE -# if CUSTOM_CACHE is None: -# CUSTOM_CACHE = CustomCache( -# loc, -# fname="bot_prod_data", -# expiry_days=500, -# clear_on_exit=False -# ) -# return CUSTOM_CACHE - -# def get_option_price_theta_data(opttick:str, -# as_of:str|datetime) -> float|None: -# """ -# Retrieve option price data from ThetaData -# """ -# as_of = pd.to_datetime(as_of) -# meta = parse_option_tick(opttick) -# data = retrieve_quote( -# symbol=meta['ticker'], -# start_date=as_of - timedelta(days=7), -# end_date=as_of + timedelta(days=1), -# strike=meta['strike'], -# right= meta['put_call'], -# exp=meta['exp_date'], -# print_url = False, -# interval='1d') -# if data is None or data.empty: -# return None - -# if as_of.date() not in data.index.date: -# raise ValueError(f"Data for {as_of.date()} not found in the retrieved data.") -# return data.loc[data.index.date == as_of.date()]['Midpoint'].values[0] - - -# def get_option_price(_id:str, date:str, force = False) -> float|None: -# """ -# Get the position price for a given position ID and date. - -# Args: -# force (bool): If True, force refresh the price from ThetaData. -# _id (str): Position ID. -# date (str): Date in 'YYYY-MM-DD' format. -# Returns: -# float|None: The position price if available, otherwise None. -# """ -# if _id not in get_custom_cache() or force: ## If not in cache or force refresh, get from ThetaData -# return get_option_price_theta_data(_id, date) -# data = get_custom_cache()[_id] -# if date not in data.index: -# print(f"No data for {_id} on {date}") -# return get_option_price_theta_data(_id, date) -# return data.loc[date, 'Midpoint'] - - -# def get_option_realtime_quote(_id:str) -> float|None: -# """ -# Get the position quote for a given position ID and date. -# """ -# meta = parse_option_tick(_id) -# return retrieve_quote_rt( -# symbol=meta['ticker'], -# exp=meta['exp_date'], -# right=meta['put_call'], -# strike=meta['strike'], -# )['Midpoint'][0] - -# def get_position_price(_id:str, date:str, force = False) -> float|None: -# """ -# Get the position price for a given position ID and date. -# Args: -# force (bool): If True, force refresh the price from ThetaData. -# _id (str): Position ID. -# date (str): Date in 'YYYY-MM-DD' format. - -# Returns: -# float|None: The position price if available, otherwise None. -# """ -# opt_ids_list = parse_position_id(_id)[1] -# prices = [] -# for leg, opt_id in opt_ids_list: - -# price = get_option_price(opt_id, date, force=force) -# if price is not None: -# prices.append(price if leg == 'L' else -price) -# else: -# print(f"No price found for {opt_id} on {date}") -# if not prices: -# print(f"No prices found for {_id} on {date}") -# return None -# return sum(prices) - - -# def get_position_realtime_quote(_id:str) -> float|None: -# """ -# Get the position quote for a given position ID. -# Args: -# _id (str): Position ID. -# Returns: -# float|None: The position quote if available, otherwise None. -# """ -# opt_ids_list = parse_position_id(_id)[1] -# quotes = [] -# for leg, opt_id in opt_ids_list: -# quote = get_option_realtime_quote(opt_id) -# if quote is not None: -# quotes.append(quote if leg == 'L' else -quote) -# else: -# print(f"No quote found for {opt_id}") -# if not quotes: -# print(f"No quotes found for {_id}") -# return None -# return sum(quotes) - - - -# def live_calculate_option_delta(opttick:str, date:str|datetime) -> float: -# """ -# Calculate the delta of an option using the binomial model. This is a live calculation that fetches the necessary data. - -# """ -# tick, option_type, exp, strike = parse_option_tick(opttick).values() -# option_price = get_option_price(opttick, date, force=True) -# timeseries = get_live_timeseries_obj() -# if tick not in timeseries.spot: -# timeseries.load_timeseries(tick, OPTION_TIMESERIES_START_DATE, datetime.now(), force=True) - -# at_index = timeseries.get_at_index(tick, date) -# s = at_index.chain_spot.close -# y = at_index.dividends -# r = at_index.rates.annualized -# vol = binomial_implied_vol( -# price=option_price, -# S=s, -# K=strike, -# r=r, -# exp_date=exp, -# option_type=option_type.lower(), -# pricing_date=date, -# dividend_yield=y) - - - -# return Calculate.delta( -# S=s, -# K=strike, -# r=r, -# sigma=vol, -# start=date, -# flag=option_type.lower(), -# exp=exp, -# y=y, -# model='binomial' -# ) - -# def live_calculate_position_delta(position_id: str, date:str|datetime) -> float: -# ids =parse_position_id(position_id)[1] -# pos_delta = 0 -# for side, opttick in ids: -# if side.upper() == 'L': -# sign = 1 -# elif side.upper() == 'S': -# sign = -1 -# else: -# raise ValueError("Invalid side in position ID. Must be 'LONG' or 'SHORT'.") -# delta = live_calculate_option_delta(opttick, date) -# pos_delta += (sign * delta) -# return pos_delta - -# def create_max_cash_map(weights: dict, -# cash: int|float, -# threshold_map: dict) -> dict: -# """ -# weights: dict of symbol -> weight (numeric) -# cash: scalar. This is initial cash for the portfolio. Not total cash as at a time. -# threshold_map: dict where keys are numeric thresholds and value is the assigned value, -# plus an optional 'else' key for fallback. -# Example: {500:4, 300:3, 200:2, 100:1, 'else': 0.5} -# Returns: dict symbol -> assigned max_cash -# """ -# # Extract numeric thresholds and sort descending -# numeric_thresholds = sorted( -# (k for k in threshold_map if isinstance(k, (int, float))), -# reverse=True -# ) -# fallback = threshold_map.get('else') -# result = {} -# for s, w in weights.items(): -# amount = w * cash -# # find first threshold that amount exceeds -# assigned = None -# for thresh in numeric_thresholds: -# if amount > thresh: -# assigned = threshold_map[thresh] -# break -# if assigned is None: -# assigned = fallback -# result[s] = assigned -# return result - -# def load_eod_tasks() -> list: -# """ -# Loads functions for eod task. Note any scheduled task must work with taking no keywords. -# ENSURE IT IS A FUNCTION THAT TAKES NO ARGUMENTS. -# """ -# with open(f"{os.environ['ALGO_DIR']}/algo/strategies/eod_tasks.yaml") as f: -# tasks = yaml.safe_load(f)['tasks'] - -# run_tasks = [] -# for task in tasks: -# module = importlib.import_module(task['module']) -# func = getattr(module, task['name'], None) -# enabled = task['enabled'] -# if func is not None and enabled: -# run_tasks.append(func) -# return run_tasks - - -# def get_prod_last_run(strat_name:str) -> pd.DataFrame: -# """ -# Retrieve the last run information for all strategies from the prod_last_run table. - -# Returns -# ------- -# pd.DataFrame -# The last run information for all strategies. -# """ -# db = DatabaseAdapter() -# data = db.query_database( -# db = 'strategy_trades_signals', -# table_name= 'prod_last_run', -# query= "SELECT * FROM strategy_trades_signals.prod_last_run WHERE strat_name = '%s'" % strat_name -# ) -# return data.run_date.max() - -# def update_prod_last_run(strat_name:str, run_date: str) -> None: -# """ -# Update a new entry to the prod_last_run table. - -# Parameters -# ---------- -# strat_name : str -# The name of the strategy. -# run_date : str -# The date and time of the run in 'YYYY-MM-DD HH:MM:SS' format. -# """ - -# dynamic_batch_update( -# db = 'strategy_trades_signals', -# table_name = 'prod_last_run', -# update_values= { -# 'run_date': pd.to_datetime(run_date).date(), -# }, -# condition={ -# 'strat_name': strat_name -# }, -# ) - -# def add_strat_to_prod_last_run(strat_name:str, run_date: str) -> None: -# """ -# Add a new strategy to the prod_last_run table if it does not already exist. - -# Parameters -# ---------- -# strat_name : str -# The name of the strategy. -# run_date : str -# The date and time of the run in 'YYYY-MM-DD HH:MM:SS' format. -# """ -# db = DatabaseAdapter() -# existing = db.query_database( -# db = 'strategy_trades_signals', -# table_name= 'prod_last_run', -# query= "SELECT * FROM strategy_trades_signals.prod_last_run WHERE strat_name = '%s'" % strat_name -# ) -# if existing.empty: -# df = pd.DataFrame({ -# 'strat_name': [strat_name], -# 'run_date': [run_date] -# }) -# db.save_to_database( -# db = 'strategy_trades_signals', -# table_name = 'prod_last_run', -# data = df, -# ) - - -# def create_strategy_signals_table(strategy_slug: str) -> None: - -# """ -# Create a table for the specified strategy if it does not already exist. - -# Parameters -# ---------- -# strategy_slug : str -# The slug of the strategy for which to create the table. -# """ -# actions = [action.value for action in Action] -# engine = get_engine('strategy_trades_signals') ## Location db -# tables = list_tables_from_db('strategy_trades_signals') ## Table in location db - -# # Check if the table already exists -# if strategy_slug not in tables: -# print(f"Creating table for strategy {strategy_slug}...") -# create_table_from_schema( -# engine, -# { -# 'table_name': strategy_slug, -# 'columns': -# [ -# {'name': "Ticker", 'type': "String", 'length': 50, 'nullable': False}, -# {'name': 'Size', 'type': 'Integer', 'nullable': False}, -# {'name': 'SIGNAL_ORIGINAL_ENTRY_TIME', 'type': 'DateTime', 'nullable': False}, -# {'name': 'SIGNAL_ORIGINAL_EXIT_TIME', 'type': 'DateTime', 'nullable': False}, -# {'name': 'SIGNAL_ID', 'type': 'String', 'length': 50, 'nullable': False}, -# {'name': 'OPEN_TODAY', 'type': 'Boolean', 'nullable': False}, -# {'name': 'CLOSE_TODAY', 'type': 'Boolean', 'nullable': False}, -# {'name': 'POSITION_PREV_OPENED', 'type': 'Boolean', 'nullable': False}, -# {'name': 'POSITION_ACTIVE', 'type': 'Boolean', 'nullable': False}, -# {'name': 'POSITION_CLOSED', 'type': 'Boolean', 'nullable': False}, -# {'name': 'SIGNAL_CLOSED', 'type': 'Boolean', 'nullable': False}, -# {'name': 'ACTION', 'type': 'Enum', 'values': actions, 'nullable': False}, -# {'name': 'RATIONALE', 'type': 'String', 'length': 255, 'nullable': True}, -# {'name': 'NEW_ENTRY_TIME', 'type': 'DateTime', 'nullable': False}, -# {'name': 'NEW_EXIT_TIME', 'type': 'DateTime', 'nullable': False}, -# ] -# } -# ) - -# else: -# print(f"Table for strategy {strategy_slug} already exists. Skipping creation.") diff --git a/.decommissioned/algo/algo.strategies.init_orders.py b/.decommissioned/algo/algo.strategies.init_orders.py deleted file mode 100644 index e3ca28e..0000000 --- a/.decommissioned/algo/algo.strategies.init_orders.py +++ /dev/null @@ -1,706 +0,0 @@ -""" -This module provides functions to initialize and manage orders for backtesting strategies. -It includes functions to set up the backtest environment, run the backtest, and manage orders and fills. -""" -raise DeprecationWarning("This module (strategies.init_orders) is deprecated and will be removed in future releases. Please use the new order management module.") -# from typing import List -# from datetime import datetime -# from copy import deepcopy -# import os -# import pandas as pd -# from pandas.tseries.offsets import BDay -# from trade.helpers.helper import ( -# change_to_last_busday, -# str_to_bool, -# ny_now -# ) -# from trade.helpers.Logging import setup_logger -# from EventDriven.backtest import OptionSignalBacktest -# from EventDriven.riskmanager.sizer import ZscoreRVolSizer, DefaultSizer -# from EventDriven.riskmanager.utils import ( -# set_timeseries_start, -# set_timeseries_end, -# set_use_temp_cache -# ) -# from EventDriven.riskmanager.utils import parse_position_id -# from EventDriven.helpers import parse_signal_id, generate_signal_id -# from EventDriven.types import SignalTypes -# from module_test.raw_code.DataManagers.DataManagers import set_skip_mysql_query, set_use_quotes -# from dbase.database.SQLHelpers import DatabaseAdapter -# from .init_strategies import get_fills -# from .enums import Action -# from .init_environ import ( -# get_custom_cache -# ) -# from .utils import (create_max_cash_map, -# get_option_price_theta_data, -# get_option_price, -# get_option_realtime_quote, -# get_position_price,) -# from ..positions.loaders.limits._limits import save_limits_from_backtester -# logger = setup_logger('strategies.init_orders') -# bkt_logger = setup_logger('strategies.backtest') - -# logger.critical("WARNING: This module (strategies.init_orders) is deprecated and will be removed in future releases. Please use the new order management module.") - -# def get_use_csv() -> bool: -# """ -# Get the value of USE_CSV. -# Returns: -# bool: The current value of USE_CSV. -# """ -# use_csv = os.environ.get('USE_CSV', 'False').lower() -# if use_csv not in ['true', '1', 'yes', 'false', '0', 'no']: -# raise ValueError("USE_CSV must be a boolean value (True/False).") -# return str_to_bool(use_csv) - -# def set_use_csv(value: bool): -# """ -# Set the value of USE_CSV. -# Args: -# value (bool): The value to set for USE_CSV. -# """ -# if value not in ['true', 'false', 1, 0, True, False, '1', '0', 'yes', 'no']: -# raise ValueError("USE_CSV must be a boolean value (True/False).") -# os.environ['USE_CSV'] = str(value).lower() -# logger.info(f"USE_CSV set to {value}") - - - -# def delete_cached_chain(tick, date): -# """ -# Delete cached chain data for a specific ticker and date. -# Args: -# tick (str): Ticker symbol. -# date (str): Date in 'YYYY-MM-DD' format. -# """ -# from EventDriven.riskmanager.utils import PERSISTENT_CACHE -# func = 'EventDriven.riskmanager.utils.populate_cache_with_chain' -# key = (func, tick, date, None, 'print_url',False) -# if key in PERSISTENT_CACHE: -# del PERSISTENT_CACHE[key] -# print(f"Deleted Chain cache for {tick} on {date}") -# else: -# print(f"No cached chain found for {tick} on {date}") - -# def delete_cached_get_order(tick, date): -# """ -# Delete cached order data for a specific ticker and date. -# Args: -# tick (str): Ticker symbol. -# date (str): Date in 'YYYY-MM-DD' format. -# """ -# from EventDriven.riskmanager.utils import PERSISTENT_CACHE -# f ='EventDriven.riskmanager.base.OrderPicker.__get_order' -# deleted = False -# for key in PERSISTENT_CACHE.keys(): -# if key[0] == f and key[1][2][1] == tick and key[2]== date: -# del PERSISTENT_CACHE[key] -# print(f"Deleted cache for {key}") -# deleted = True -# if not deleted: -# print(f"No cached order found for {tick} on {date}") - - -# def generate_trades_data(strategy_folder_name: str, date:str|datetime=None) -> tuple[pd.DataFrame, list[str]]: -# """ -# Generate trades data from the strategy folder. -# Args: -# strategy_folder_name (str): Name of the strategy folder. -# Returns: -# tuple: A tuple containing: -# - pd.DataFrame: DataFrame with trade details. -# - list[str]: List of unique signal IDs.""" -# db = DatabaseAdapter() -# if get_use_csv(): -# logger.info(f"Using CSV for trades data in {strategy_folder_name}") -# if date: -# logger.critical("Date parameter is ignored when USE_CSV is True.") -# trades = pd.read_csv(f"{os.environ['ALGO_DIR']}/algo/strategies/{strategy_folder_name}/trades.csv") -# else: -# logger.info(f"Using database for trades data in {strategy_folder_name}") -# if date: -# logger.info(f"Filtering trades data for date: {date}") -# trades = db.query_database(db='strategy_trades_signals', -# table_name= 'historical_signals', -# query= f"SELECT * FROM strategy_trades_signals.historical_signals WHERE RUN_DATE = '{pd.to_datetime(date).strftime('%Y-%m-%d')}'") -# if trades.empty: -# logger.warning(f"No trades found for date: {date}. Falling back to all trades.") -# print(f"No trades found for date: {date}. Falling back to all trades.") -# # trades = db.query_database(db='strategy_trades_signals', -# # table_name= strategy_folder_name, -# # query= f"SELECT * FROM {strategy_folder_name}") -# else: -# trades = db.query_database(db='strategy_trades_signals', -# table_name= strategy_folder_name, -# query= f"SELECT * FROM {strategy_folder_name}") -# consumption_trades = trades[trades['ACTION'].isin([Action.OPEN.value, Action.CLOSE.value, Action.HOLD.value])].copy() -# consumption_trades=consumption_trades[['Ticker', 'Size', 'NEW_ENTRY_TIME', 'NEW_EXIT_TIME', 'SIGNAL_ID', 'ACTION']] -# consumption_trades.rename(columns={ -# 'NEW_ENTRY_TIME': 'EntryTime', -# 'NEW_EXIT_TIME': 'ExitTime', -# 'SIGNAL_ID': 'PT_BKTEST_SIG_ID'}, inplace=True) -# consumption_trades['ExitTime'] = consumption_trades['ExitTime'].fillna(ny_now().strftime('%Y-%m-%d')) -# consumption_trades['EntryTime'] = consumption_trades['EntryTime'].fillna(ny_now().strftime('%Y-%m-%d')) -# consumption_trades['MAP_ORDER_SIGNAL_ID'] = consumption_trades.apply(lambda x: generate_signal_id( -# x['Ticker'], -# x['EntryTime'], -# SignalTypes.LONG.value if x['Size'] > 0 else SignalTypes.SHORT.value -# ), axis=1) -# signals = consumption_trades.PT_BKTEST_SIG_ID.unique().tolist() -# return consumption_trades, signals - - -# def delete_cached_chain_and_order(trades: pd.DataFrame): -# """ -# Delete cached chain and order data for each trade in the DataFrame. -# Args: -# trades (pd.DataFrame): DataFrame containing trade details. -# """ -# signals = trades.to_dict(orient='records') -# for signal in signals: -# date = change_to_last_busday(pd.to_datetime(signal['EntryTime']) + BDay(1), offset=-1).strftime('%Y-%m-%d') -# delete_cached_chain(signal['Ticker'], date) -# delete_cached_get_order(signal['Ticker'], date) - -# def add_attr(attr_config, obj, skip_keys=[]): -# """ -# Add attributes to an object based on a configuration dictionary. -# attr_config: dict where keys are attribute names and values are the values to set. -# obj: the object to which attributes will be added. -# skip_keys: list of keys to skip in the attr_config. -# """ -# for attr, value in attr_config.items(): -# if attr in skip_keys: -# continue -# assert hasattr(obj, attr), f"{obj.__class__.__name__} does not have `{attr}`" -# setattr(obj, attr, value) - - - -# def transfer_processed_data_to_cache(bkt:OptionSignalBacktest): -# """ -# Transfer processed option data to the custom cache. -# """ -# for name, data in bkt.risk_manager.processed_option_data.items(): -# if name not in get_custom_cache().keys(): ## TODO: Should transfer all. But data is inconsistent at times. Fix this -# get_custom_cache()[name] = data -# print(f"Transferred processed data for {name} to custom cache.") - - -# def setup_backtest_env( -# trades: pd.DataFrame, -# portfolio_config: dict, -# rm_config: dict, -# sizer_settings: dict, -# config: dict, -# weights: dict = {}, -# cash: int|float=20_000, -# skip_keys_map: dict = {}, -# bkt_config: dict = {} -# ): -# """ -# Set up the backtest environment with the given configuration and trades. - -# Args: -# trades (pd.DataFrame): DataFrame containing trades data. -# portfolio_config (dict): Configuration for the portfolio. -# rm_config (dict): Configuration for the risk manager. -# sizer_settings (dict): Settings for the sizer. -# config (dict): General configuration for the backtest. -# weights (dict, optional): Weights for the portfolio. Defaults to {}. -# cash (int|float, optional): Initial cash amount. Defaults to 20_000. -# skip_keys_map (dict, optional): Keys to skip in the configuration. Defaults to {}. -# bkt_config (dict, optional): Additional configuration for the backtest. Defaults to {}. -# Returns: -# OptionSignalBacktest: An instance of the OptionSignalBacktest class with the configured environment. -# """ - -# ## Set up backtest environment. -# ## NOTE: Need to delete processed_option_data & position_data in other to allow updating the time stamps -# portfolio_config, rm_config, bkt_config = deepcopy(portfolio_config), deepcopy(rm_config), deepcopy(bkt_config) -# set_timeseries_start(config['rm_series_start']) -# set_timeseries_end(config['rm_series_end']) -# set_skip_mysql_query(True) ## To avoid time lag in getting option Timeseries -# set_use_temp_cache(True) ## To ensure we use the temp cache for this backtest. - -# for key in skip_keys_map: -# assert key in ['rm_config', 'portfolio_config', 'bkt_config'], f"Invalid key in skip_keys_map: {key}. Expected keys are ['rm_config', 'portfolio_config', 'bkt_config']" - -# for key in ['rm_config', 'portfolio_config', 'bkt_config']: ## Creating a default -# if key not in skip_keys_map: -# skip_keys_map[key] = [] - -# ## Apply weight haircut to weights -# weights = {x: -# v * portfolio_config.get('weights_haircut', 1) for x,v in weights.items()} -# if 'weights_haircut' not in portfolio_config: -# logger.warning("weights_haircut not found in portfolio_config, using default value of 1.0") - -# ## Produce max_cash_map -# cash_map = portfolio_config.pop('max_cash_map', {}) -# portfolio_config['max_contract_price'] = create_max_cash_map( -# weights=weights, -# cash=cash, -# threshold_map=cash_map -# ) - -# ## Set up the portfolio & Backtest -# bkt = OptionSignalBacktest( -# trades=trades, -# initial_capital=cash, -# t_plus_n=portfolio_config.pop('t_plus_n'), -# symbol_list=config['traded_symbols'], -# finalize_trades = False) - -# ## Clear any existing processed option data and position data then upload new one -# bkt.risk_manager.clear_caches() -# bkt.risk_manager.clear_core_data_caches() ## Clear core data caches to ensure fresh data is used -# bkt.risk_manager.append_option_data(data_pack=get_custom_cache()) - -# ## Set Enabled Limits: -# if 'limits_enabled' in rm_config: -# for lmt in rm_config['limits_enabled']: -# if lmt not in bkt.risk_manager.limits: -# raise ValueError(f"Limit {lmt} not found in risk manager limits.") -# bkt.risk_manager.limits[lmt]=True - -# ## Set up the Sizer in RM -# if 'sizer_type' in rm_config: -# _type = rm_config.pop('sizer_type') -# if _type == 'ZscoreRVolSizer': -# rm_config['sizer'] = ZscoreRVolSizer(pm=bkt.portfolio, -# rm=bkt.risk_manager, -# **sizer_settings) -# elif _type == 'DefaultSizer': -# rm_config['sizer'] = DefaultSizer(pm=bkt.portfolio, -# rm=bkt.risk_manager, -# **sizer_settings) -# else: -# raise ValueError(f"Invalid sizer type: {_type}. Expected 'ZscoreRVolSizer' or 'DefaultSizer'.") -# else: -# logger.info("No sizer type specified in rm_config, using DefaultSizer in RiskManager as default.") - -# ## Add Attributes to the objects -# add_attr(rm_config, -# bkt.risk_manager, -# skip_keys=['rm_series_start', 'rm_series_end'] + skip_keys_map['rm_config']) -# add_attr(portfolio_config, bkt.portfolio, skip_keys=['initial_cash'] + skip_keys_map['portfolio_config']) -# add_attr(bkt_config, bkt.portfolio, skip_keys=['initial_cash', 'weights_haircut']) -# bkt.logger = bkt_logger # Set the logger for the backtest - -# return bkt - - -# def run_backtest( -# trades: pd.DataFrame, -# portfolio_config: dict, -# rm_config: dict, -# sizer_settings: dict, -# config: dict, -# T_0: str, -# strat_name: str, -# strateg_slug: str, -# weights: dict = {}, -# cash: int|float=20_000, -# skip_keys_map: dict = {}, -# bkt_config: dict = {} -# ): -# """ -# Run the backtest with the given configuration and trades. -# Args: -# trades (pd.DataFrame): DataFrame containing trades data. -# portfolio_config (dict): Configuration for the portfolio. -# rm_config (dict): Configuration for the risk manager. -# sizer_settings (dict): Settings for the sizer. -# config (dict): General configuration for the backtest. -# weights (dict, optional): Weights for the portfolio. Defaults to {}. -# cash (int|float, optional): Initial cash amount. Defaults to 20_000. -# skip_keys_map (dict, optional): Keys to skip in the configuration. Defaults to {}. -# bkt_config (dict, optional): Additional configuration for the backtest. Defaults to {}. -# Returns: -# OptionSignalBacktest: An instance of the OptionSignalBacktest class with the configured environment. - -# Sequence: -# 1. Prepare the data for backtest. Which includes: -# - Loading the trades data. -# - Adjusting latest date to match the current date. -# 2. Set up the backtest environment with the given configuration and trades. -# 3. Run the backtest. -# 4. Transfer processed data to the custom cache for future use. -# 5. Run post test tasks, which involves deleting today's data from the cache. It will be re-added EOD with eod_task -# 6. Return the backtest object for further analysis or inspection. - -# """ - -# bkt = setup_backtest_env( -# trades=trades, -# portfolio_config=portfolio_config, -# rm_config=rm_config, -# sizer_settings=sizer_settings, -# config=config, -# weights=weights, -# cash=cash, -# skip_keys_map=skip_keys_map, -# bkt_config=bkt_config -# ) -# try: -# set_use_quotes(True) -# bkt.run() # Run the backtest -# set_use_quotes(False) -# except KeyError: -# logger.error("Backtest complete, key error coming from T+1 not available") -# print("Backtest complete, key error coming from T+1 not available") -# except Exception as e: -# raise e - -# ## Intra day needs to use quotes for backtest, not EOD. -# ## T_0 is the run date of the order search -# # t1 = change_to_last_busday(pd.to_datetime(T_0) + BDay(config['t_plus_n']), offset=-1).strftime('%Y-%m-%d') -# closed_orders = get_close_orders(trades, strateg_slug) -# open_orders = get_open_orders(bkt, T_0) -# actions=get_actions(bkt, T_0) - -# ## Save limits -# save_limits_from_backtester(bkt, T_0) - -# # Transfer processed data to the custom cacheOy -# print("Backtest completed. Transferring processed data to cache...") -# transfer_processed_data_to_cache(bkt) -# print("Processed data transferred to cache. Running post test tasks...") - -# return { -# 'BACKTESTER': bkt, -# 'CLOSED_ORDERS': closed_orders, -# 'OPEN_ORDERS': open_orders, -# 'ACTIONS': actions, -# } - - -# def get_close_orders_meta(consumption_trades:pd.DataFrame, strat_name:str) -> List[dict]: -# """ -# Get the close orders meta data for the given consumption trades DataFrame. -# Args: -# consumption_trades (pd.DataFrame): DataFrame containing trades data with columns 'ACTION', 'PT_BKTEST_SIG_ID', etc. -# strat_name (str): Name of the strategy to filter the trades. -# Returns: -# List[dict]: A list of dictionaries containing the close orders meta data. -# """ -# ## Get closed trades signal_id -# closed_signals = consumption_trades[consumption_trades['ACTION'] == Action.CLOSE.value]['PT_BKTEST_SIG_ID'].unique().tolist() - -# ## Get fills for the closed trades -# fills = get_fills(closed_signals, strat_name) - -# ## Get only open fills. -# open_fills = fills[fills['position_effect'] == Action.OPEN.value].copy() - -# ## Create order meta: -# close_today_signals=[] -# for i, row in open_fills.iterrows(): -# signal_id = row['signal_id'] -# signal_meta = parse_signal_id(signal_id) -# meta= dict( -# signal_id=signal_id, -# position_id = row['position_id'], -# submitted_timestamp=ny_now(), -# ticker=signal_meta['ticker'], -# direction=SignalTypes.LONG.value if row['direction']>0 else SignalTypes.SHORT.value, -# order_type='GTC', -# quantity=row['quantity'], -# limit_price=get_position_price(row['position_id'], -# ny_now().strftime('%Y-%m-%d')), -# fill_ts=None, -# fill_price=None, -# filled_qty=None, -# position_effect='CLOSE', -# strategy_name='LongBBandsTrend_SL' -# ) -# close_today_signals.append(meta) -# return close_today_signals - -# def get_close_orders(consumption_trades:pd.DataFrame, strat_name:str) -> dict: -# """ -# Get the close orders for the given close today order meta data. -# Args: -# close_today_order_meta (List[dict]): List of dictionaries containing the close orders meta data. -# Gotten from get_close_orders_meta function. -# Returns: -# dict: A dictionary where keys are tickers and values are dictionaries containing order details. -# """ - -# close_order_list={} -# consumption_trades = consumption_trades.copy() -# consumption_trades['signal_id'] = consumption_trades['PT_BKTEST_SIG_ID'] - -# ## Get closed trades signal_id -# closed_signals = consumption_trades[consumption_trades['ACTION'] == Action.CLOSE.value]['PT_BKTEST_SIG_ID'].unique().tolist() -# if not closed_signals: -# print("No closed trades found.") -# return close_order_list - -# ## Get fills for the closed trades -# fills = get_fills(closed_signals, strat_name) - -# ## Get only open fills. -# open_fills = fills[fills['position_effect'] == Action.OPEN.value].copy() - -# for idx, sig in open_fills.iterrows(): -# map_signal_id = consumption_trades[consumption_trades['signal_id'] == sig['signal_id']]\ -# ['PT_BKTEST_SIG_ID'].unique()[0] -# size=consumption_trades[consumption_trades['PT_BKTEST_SIG_ID'] == map_signal_id]['Size'].values[0] -# meta= parse_signal_id(sig['signal_id']) -# pairs=parse_position_id(sig['trade_id'])[1] -# order=dict( -# result='SUCCESSFUL', -# data=dict( -# trade_id=sig['trade_id'], -# close=get_position_price(sig['trade_id'], -# ny_now().strftime('%Y-%m-%d')), -# long=[x[1] for x in pairs if x[0] == 'L'], -# short=[x[1] for x in pairs if x[0] == 'S'], -# quantity=sig['quantity'], -# ), -# signal_id=sig['signal_id'], -# direction=SignalTypes.LONG.value if size > 0 else SignalTypes.SHORT.value, -# map_signal_id=map_signal_id, -# ) -# close_order_list[meta['ticker']] = order - -# return close_order_list - - - -# def get_open_orders(bkt:OptionSignalBacktest, t1: str) -> dict: -# """ -# Extract orders from the backtest object for a specific date. -# Args: -# bkt (OptionSignalBacktest): The backtest object containing the risk manager. -# t1 (str): The date for which to extract orders, formatted as 'YYYY-MM-DD'. -# Returns: -# dict: A dictionary where keys are tickers and values are dictionaries containing order details. -# """ -# if not isinstance(t1, str): -# if isinstance(t1, datetime): -# t1 = t1.strftime('%Y-%m-%d') -# else: -# raise ValueError("t1 must be a string in 'YYYY-MM-DD' format or a datetime object.") - -# orders = bkt.risk_manager.order_cache.get(t1, {}) -# unadjusted_trades = bkt.unadjusted_trades -# for order in orders.values(): -# opt_signal_id = order['signal_id'] -# order['map_signal_id'] = unadjusted_trades[unadjusted_trades['signal_id'] == opt_signal_id]['PT_BKTEST_SIG_ID'].unique()[0] -# return orders - - -# def get_open_orders_meta(bkt:OptionSignalBacktest, T_1: str) -> List[dict]: -# """ -# Get open orders metadata -# for the backtest on a specific date. -# Args: -# bkt (OptionSignalBacktest): The backtest object containing the risk manager. -# T_1 (str): The date for which to extract open orders, formatted as 'YYYY-MM-DD'. -# Returns: -# """ -# save_to_df = [] -# unadjusted_trades = bkt.unadjusted_trades -# _open_orders = get_open_orders(bkt, T_1) - -# ## Open Orders -# for tick, order in _open_orders.items(): -# meta=dict( -# signal_id=unadjusted_trades[unadjusted_trades['signal_id'] == order['signal_id']]['PT_BKTEST_SIG_ID'].unique()[0], -# position_id=order['data']['trade_id'], -# submitted_timestamp=ny_now(), -# ticker=tick, -# direction=order['direction'], -# order_type='GTC', -# quantity=order['data']['quantity'], -# limit_price=order['data']['close'], -# fill_ts=None, -# fill_price=None, -# filled_qty=None, -# position_effect='OPEN', -# strategy_name='LongBBandsTrend_SL' -# ) -# save_to_df.append(meta) -# return save_to_df - -# def get_actions(bkt:OptionSignalBacktest, T_1: str) -> dict: -# """ -# Get actions from the risk manager for a specific date. -# Args: -# bkt (OptionSignalBacktest): The backtest object containing the risk manager. -# T_1 (str): The date for which to extract actions, formatted as 'YYYY-MM-DD'. -# Returns: -# dict: A dictionary where keys are tickers and values are dictionaries containing action details. -# """ -# actions = [v for x, v in bkt.risk_manager._actions.items() if x.strftime('%Y-%m-%d') == T_1] -# actions = actions[0] if actions else {} -# return actions - -# ## Save Utils -# def save_orders_to_database(open_orders_meta: List[dict], -# close_order_meta: List[dict]) -> pd.DataFrame: -# """ -# Save open and exit orders to the database. -# Args: -# open_orders_meta (List[dict]): List of dictionaries containing open orders metadata. -# gotten from get_open_orders_meta function. -# close_order_meta (List[dict]): List of dictionaries containing close orders metadata. -# gotten from get_close_orders_meta function. -# Returns: -# pd.DataFrame: DataFrame containing the saved orders metadata. - -# """ -# save_to_df=open_orders_meta+close_order_meta -# db=DatabaseAdapter() -# db.save_to_database( -# data=pd.DataFrame(save_to_df), -# table_name='orders', -# db='portfolio_data', -# filter_data=False, -# _raise=True) -# return pd.DataFrame(save_to_df) - -# def _save_to_database_helper( -# consumption_trades: pd.DataFrame, -# strat_name: str, -# bkt: OptionSignalBacktest, -# T_1: str -# ): -# """ -# Helper function to save orders to the database. -# Args: -# consumption_trades (pd.DataFrame): DataFrame containing trades data. -# strat_name (str): Name of the strategy. -# bkt (OptionSignalBacktest): The backtest object. -# T_1 (str): The date for which to extract orders, formatted as 'YYYY-MM-DD'. -# Returns: -# None -# """ -# close_today_order_meta = get_close_orders_meta(consumption_trades, strat_name) -# open_orders_meta = get_open_orders_meta(bkt, T_1) - -# if not open_orders_meta and not close_today_order_meta: -# print("No open or close orders to save.") -# return -# print(f"Saving {len(open_orders_meta)} open orders and {len(close_today_order_meta)} close orders to database.") -# if not open_orders_meta: -# print("No open orders to save.") -# if not close_today_order_meta: -# print("No close orders to save.") -# # Save to database -# return save_orders_to_database(open_orders_meta, close_today_order_meta) - - -# def make_fill_meta( -# signal_id: str, -# strategy_name: str, -# position_id: str, -# fill_price: float, -# fill_timestamp: datetime, -# quantity: int|float, -# position_effect: str, -# direction: str, -# ticker: str, -# order_type: str, -# limit_price: float, -# filled_qty: int|float -# ): -# """ -# Create a fill meta dictionary for the order. -# """ -# assert position_effect in ['OPEN', 'CLOSE'], "position_effect must be either 'OPEN' or 'CLOSE'" -# assert direction in ['LONG', 'SHORT'], "direction must be either 'LONG' or 'SHORT'" -# return { -# 'signal_id': signal_id, -# 'strategy_name': strategy_name, -# 'position_id': position_id, -# 'fill_price': fill_price, -# 'fill_timestamp': fill_timestamp, -# 'quantity': quantity, -# 'position_effect': position_effect, -# 'direction': direction, -# 'ticker': ticker, -# 'order_type': order_type, -# 'limit_price': limit_price, -# 'filled_qty': filled_qty -# } - -# def save_fills_to_database(fills_list_meta: List[dict]): -# """ -# Save fills to the database. -# """ -# db=DatabaseAdapter() -# db.save_to_database( -# data=pd.DataFrame(fills_list_meta), -# table_name='fills', -# db='portfolio_data', -# filter_data=False, -# _raise=True -# ) - -# return True - - - -# ########## NEW FUNCTIONS ########## -# from ..positions.loaders.configs import get_configs -# from EventDriven.riskmanager._order_validator import build_inputs_with_config, OrderInputs, OrderSchema -# from EventDriven.riskmanager.market_data import get_timeseries_obj, OPTION_TIMESERIES_START_DATE -# CONFIGS = get_configs() - -# def load_position_actions(slug:str, test:bool = False) -> pd.DataFrame: -# trades = generate_trades_data(slug)[0] -# if not test: -# return trades[trades.ACTION == 'OPEN'],trades[trades.ACTION == 'CLOSE'] -# else: -# print("WARNING: TEST MODE. USING INCORRECT INFORMATION IN load_position_actions function") -# close = trades.copy() -# close['ACTION'] = 'CLOSE' -# close['EntryTime'] = datetime.now() - BDay(1) -# close['ExitTime'] = datetime.now() - BDay(1) -# opens = trades.copy() -# opens['ACTION'] = 'OPEN' -# opens['EntryTime'] = datetime.now() - BDay(1) -# opens['ExitTime'] = datetime.now() - BDay(1) -# return opens, close - -# def load_timeseries_for_trades(sym_list: List[str], force=False) -> None: -# timeseries = get_timeseries_obj() -# for sym in sym_list: -# load_bool = sym not in timeseries.spot \ -# or sym not in timeseries.chain_spot \ -# or sym not in timeseries.dividends \ -# or force - -# if load_bool: -# timeseries.load_timeseries(sym, OPTION_TIMESERIES_START_DATE, datetime.now(), force=force) - - -# def get_max_cash_for_symbol(sym: str, slug: str) -> float: -# return CONFIGS.get_configs(slug).cash_map[sym] - - -# def build_inputs(slug: str, -# row: pd.Series, -# tick: str) -> tuple[OrderSchema, OrderInputs]: -# """ -# Builds the inputs for the order selection engine based on the strategy slug and trade row. -# Args: -# slug (str): The strategy slug. -# row (pd.Series): The trade row containing trade details. Expected keys: ['PT_BKTEST_SIG_ID', 'Size', 'EntryTime'] -# tick (str): The stock ticker symbol. -# date (str|datetime): The date for the order selection. - -# Returns: -# Tuple[OrderSchema, OrderInputs]: A tuple containing the OrderSchema and OrderInputs dataclass. -# """ - -# ## Build Config for the strategy slug -# config = CONFIGS.get_configs(slug) - -# ## This is the max price for the order search engine -# max_close = get_max_cash_for_symbol(tick, slug) \ No newline at end of file diff --git a/.decommissioned/algo/algo.stratetgies.init_orders_new_init_orders.py b/.decommissioned/algo/algo.stratetgies.init_orders_new_init_orders.py deleted file mode 100644 index 8966072..0000000 --- a/.decommissioned/algo/algo.stratetgies.init_orders_new_init_orders.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -This module provides functions to initialize and manage orders for backtesting strategies. -It includes functions to set up the backtest environment, run the backtest, and manage orders and fills. -""" -raise DeprecationWarning("This module (strategies.init_orders_new._init_orders) is deprecated and will be removed in future releases. Please use the new order management module.") -# from typing import List -# from datetime import datetime -# from copy import deepcopy -# import os -# import pandas as pd -# from pandas.tseries.offsets import BDay -# from trade.helpers.helper import ( -# change_to_last_busday, -# str_to_bool, -# ny_now -# ) -# from trade.helpers.Logging import setup_logger -# from EventDriven.backtest import OptionSignalBacktest -# from EventDriven.riskmanager.sizer import ZscoreRVolSizer, DefaultSizer -# from EventDriven.riskmanager.utils import ( -# set_timeseries_start, -# set_timeseries_end, -# set_use_temp_cache -# ) -# from EventDriven.riskmanager.utils import parse_position_id -# from EventDriven.helpers import parse_signal_id, generate_signal_id -# from EventDriven.types import SignalTypes -# from module_test.raw_code.DataManagers.DataManagers import set_skip_mysql_query, set_use_quotes -# from dbase.database.SQLHelpers import DatabaseAdapter -# from ..init_strategies import get_fills -# from ..enums import Action -# from ..init_environ import ( -# get_custom_cache -# ) -# from ..utils import (create_max_cash_map, -# get_option_price_theta_data, -# get_option_price, -# get_option_realtime_quote, -# get_position_price,) -# from ...positions.loaders.limits._limits import save_limits_from_backtester -# logger = setup_logger('strategies.init_orders_new._init_orders') -# bkt_logger = setup_logger('strategies.backtest') - -# logger.critical("WARNING: This module (strategies.init_orders_new._init_orders) is deprecated and will be removed in future releases. Please use the new order management module.") - - - - - - - - - - diff --git a/.decommissioned/strats.py b/.decommissioned/strats.py deleted file mode 100644 index 4a06c70..0000000 --- a/.decommissioned/strats.py +++ /dev/null @@ -1,179 +0,0 @@ -from backtesting.lib import crossover -import pandas_ta as ta -import os -import sys -from backtesting.backtesting import Strategy -import pandas as pd - - -def shift(series,n): - pd.Series(series) - return pd.Series(series).shift(n) - - -class BBandsTrend2(Strategy): - start_date = None - outter_band = 2 - inner_band = 0.5 - length = 190 - exit_ma = 35 - stop_loss = 0.1 - long_close = False - long_close_counter = 0 - long_open = False - long_open_counter = 0 - short_open_ = False - short_open_counter = 0 - short_close_ = False - short_close_counter = False - open_wait_days = 0 - close_wait_days =0 - gapper_limit = 1000 - start, end, interval = '2000-06-08', '2024-06-29', '1d' - - - def init(self): - #COMPUTE MOVING AVERAGES FOR STRATEGY - bbands_outter = ta.bbands(pd.Series(self.data.Close), length=self.length, std=self.outter_band, mamode='sma') - bbands_inner = ta.bbands(pd.Series(self.data.Close), length=self.length, std=self.inner_band, mamode='sma') - macd = ta.macd(pd.Series(self.data.Close), 12,26,9) - adx = ta.adx(pd.Series(self.data.High), pd.Series(self.data.Low), pd.Series(self.data.Close), 21, mamode='wilders') - # Assign bands to instance variables - self.lower_inner_band = self.I(lambda: bbands_inner[f'BBL_{int(self.length)}_{float(self.inner_band)}'], name = 'lower_inner_band', color = 'blue', overlay = True) - self.middle_inner_band = self.I(lambda: bbands_inner[f'BBM_{int(self.length)}_{float(self.inner_band)}'], name = 'middle_inner_band') - self.upper_inner_band = self.I(lambda: bbands_inner[f'BBU_{int(self.length)}_{float(self.inner_band)}'], name = 'upper_inner_band', color = 'blue', overlay= True) - # self.sp500 = self.I(create_datasate,['SPY'], self.start, '1d',end = self.end , return_object=False) - self.MACD_Hist = self.I(lambda: macd['MACDh_12_26_9'], name = 'MACD HIS') - self.ADX = self.I(lambda: adx['ADX_21']) - - self.exit_ma = self.I(ta.ema, pd.Series(self.data.Close), length=self.exit_ma) - - def next(self): - price = self.data.Close[-1] - date = self.data.index[-1] - macd = self.MACD_Hist[-1] - adx = self.ADX[-1] - upper = self.upper_inner_band[-1] - lower = self.lower_inner_band[-1] - date = self.data.index[-1] - middle = self.middle_inner_band[-1] - exit_ma = self.exit_ma[-1] - up_gap_ = abs((upper / exit_ma) - 1)*100 - - - - - - print(date, ':','Open', self.long_open, self.long_open_counter) if self.long_open_counter > 51 else None - print(date, ':','Close', self.long_close, self.long_close_counter) if self.long_close_counter > 51 else None - # Check for entry crossover from below to above - if price > upper and not self.long_open: - # print('Set Long Flag to True', date) - self.long_open = True - self.long_open_counter += 1 - - # Check for exit crossover from below - if price < upper and not self.long_close: - self.long_close = True - self.long_close_counter = True - - # If Price goes below entry crossover, reset entry flags - if price < upper and self.long_open: - # print('Set Long Flag to False', date) - self.long_open = False - self.long_open_counter = 0 - - - #If price goes back above exit crossover, reset exit flags - if price > upper and self.long_close: - # print('Set Close Long Flag to True', date) - self.long_close = False - self.long_close_counter = 0 - - # Increment open counter if entry is still valid - if self.long_open: - # print('Hi') - self.long_open_counter += 1 - - - #Increment close counter if exit is still valid - if self.long_close: - self.long_close_counter += 1 - - # Enter a trade after waiting period if no position is open - if self.long_open and self.long_open_counter >= self.open_wait_days: - if not self.position: - # print('Opening Long', date) - self.buy(sl=self.data.Close[-1] * (1 - self.stop_loss)) - self.long_open = False - self.long_open_counter = 0 - - #Exit a trade after waiting period if position is still open - if self.long_close and self.long_close_counter >= self.close_wait_days: - if self.position: - # print('Closing Long', date) - self.position.close() - self.long_close = False - self.long_close_counter = 0 - - -class MAStrat(Strategy): - trend_ma = 71 - entry_ma = 36 - exit_ma_v = 20 - stop_loss = 0.30 # 2% stop loss - take_profit = 0.15 - shift = 4 - - def init(self): - #COMPUTE MOVING AVERAGES FOR STRATEGY - # self.ma_trend = self.I(ta.ma, "ema", pd.Series(self.data.Close), length = self.trend_ma ) - self.ma_entry = self.I(ta.ma, "ema", pd.Series(self.data.Close), length = self.entry_ma ) - self.exit_ma = self.I(ta.ma, "ema", pd.Series(self.data.Close), length = self.exit_ma_v ) - self.ma_shifter = self.I(shift, self.exit_ma, self.shift) - self.close_shifter = self.I(shift, self.data.Close, self.shift) - # self.trend_ma = self.I(ta.ma, "ema", pd.Series(self.data.Close), length = self.trend_ma ) - - def next(self): - shifted = self.ma_shifter[-1] - price = self.data.Close[-1] - date = self.data.index[-1] - entry = self.ma_entry[-1] - close_shifted = self.close_shifter[-1] - # trend = self.trend_ma[-1] - # print(self.data.Next_Day_Open[-1]) - # print(entry, shifted) - - #IF WE DON'T ALREADY HAVE A POSITION - if crossover(self.data.Close,self.ma_entry ) and not self.position and entry >= shifted and price >= close_shifted: - self.buy(sl=self.data.Close[-1] * (1 - self.stop_loss)) - - - elif (self.position and price < self.exit_ma) : - self.position.close() - - if self.ma_entry > self.data.Close and not self.position: - self.sell() - - elif (self.position and price > self.exit_ma) : - self.position.close() - - - - - - - # elif price < trend: - - # if crossover(self.ma_entry, self.data.Close ) and not self.position and price < entry: - # self.sell() - - # elif (self.position and price > self.exit_ma): - # self.position.close() - - - # FIGURE OUT HOW TO USE CROSS - elif not self.position and crossover(self.data.Close,self.exit_ma ) : - # RE-ENTER TRADE AS LONG AS ABOVE 21 SMA & CROSS OVER 13 - self.buy(sl=self.data.Close[-1] * (1 - self.stop_loss)) - diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..4643047 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,156 @@ +# QuantTools DataManager System - Copilot Instructions + +## Project Overview +This is a quantitative trading system focused on options pricing and risk management. + +## Code Style & Standards + +### Type Hints +- Always use complete type hints for all function parameters and return values +- Use `Union[datetime, str]` for date parameters that accept multiple formats +- Use `Optional[T]` for nullable parameters +- Import types from `typing`: `Optional, Union, List, Dict, Tuple, ClassVar` + +### Date/Time Conversion +- **Always use `to_datetime` from `trade.helpers.helper` for datetime conversions** +- Never use `datetime.strptime()` or `pd.to_datetime()` directly +- Import: `from trade.helpers.helper import to_datetime` +- Handles both single values and iterables +- Tries "%Y-%m-%d" format first, then lets pandas guess if that fails +- Supports optional `format` parameter for custom formats + +**Example:** +```python +from trade.helpers.helper import to_datetime + +# Single string conversion +date_obj = to_datetime("2026-01-15") + +# With custom format +date_obj = to_datetime("15-01-2026", format="%d-%m-%Y") + +# Iterable conversion +dates = to_datetime(["2026-01-15", "2026-01-16", "2026-01-17"]) + +# Already datetime - returns as-is +date_obj = to_datetime(datetime.now()) +``` + +### Docstrings +- Use Google-style docstrings for all classes and methods +- Include Args, Returns, Raises, and Examples sections +- Examples should be executable and demonstrate real-world usage + +**Example:** +```python +def get_forward_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + maturity_date: Union[datetime, str], + div_type: Optional[DivType] = None, + *, + dividend_result: Optional[DividendsResult] = None, + use_chain_spot: bool = True, +) -> ForwardResult: + """Returns daily forward prices from valuation dates to maturity. + + Computes forward prices for each business day in [start_date, end_date], + where each forward is valued to the fixed maturity_date. Uses discrete + dividends (Schedule objects) or continuous yields depending on div_type. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + maturity_date: Fixed horizon date for all forwards (e.g., option expiry). + div_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to DISCRETE. + dividend_result: Pre-computed dividend data. If None, fetches internally. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + + Returns: + ForwardResult containing daily_discrete_forward or daily_continuous_forward + Series with DatetimeIndex, plus the dividend_result used and cache key. + + Raises: + ValueError: If maturity_date < start_date. + ValueError: If dividend_result.undo_adjust != use_chain_spot. + + Examples: + >>> # Basic usage with automatic dividend fetching + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> result = fwd_mgr.get_forward_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... div_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + >>> print(result.daily_discrete_forward.head()) + datetime + 2025-01-02 155.32 + 2025-01-03 156.01 + ... + + >>> # Provide pre-computed dividends for efficiency + >>> div_mgr = DividendDataManager("AAPL") + >>> div_result = div_mgr.get_schedule_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... undo_adjust=True + ... ) + >>> fwd_result = fwd_mgr.get_forward_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_result=div_result, + ... use_chain_spot=True + ... ) + """ +``` + +### Naming Conventions +- **Classes:** + - Managers end with `Manager`: `DividendDataManager`, `RatesDataManager` + - Results end with `Result`: `DividendsResult`, `ForwardResult`, `RatesResult` + - Configs end with `Config`: `DividendsConfig` +- **Methods:** + - Use `get_*` for retrieval methods: `get_schedule()`, `get_rate()` + - Use `_load_*` for private loading helpers: `_load_spot()`, `_load_rates()` + - Use `_compute_*` for calculation methods: `_compute_forward_discrete()` +- **Variables:** + - Use `_str` suffix for string dates: `start_str`, `end_str`, `mat_str` + - Use `_dt` suffix for date objects: `start_dt`, `end_dt`, `mat_dt` + +### Dataclasses +- Prefer `@dataclass` over regular classes for data containers +- Use pydantic `@dataclass` for validation when needed (Only on strict data models) + - Import from `pydantic.dataclasses` for pydantic dataclasses and alias as `pydantic_dataclass` +- Result classes should inherit from base `Result` class +- Use `frozen=True, slots=True` for immutable configs (e.g., `CacheSpec`) + +**Example:** +```python +@dataclass(frozen=True, slots=True) +class CacheSpec: + """Configuration for cache initialization.""" + base_dir: Optional[Path] = DM_GEN_PATH.as_posix() + default_expire_days: Optional[int] = 500 + default_expire_seconds: Optional[int] = None + cache_fname: Optional[str] = None + clear_on_exit: bool = False + +@dataclass +class DividendsResult(Result): + """Result container for dividend data.""" + daily_discrete_dividends: Optional[pd.Series] = None + daily_continuous_dividends: Optional[pd.Series] = None + dividend_type: Optional[DivType] = None + key: Optional[str] = None + undo_adjust: Optional[bool] = None + + def is_empty(self) -> bool: + if self.dividend_type == DivType.DISCRETE: + return self.daily_discrete_dividends is None or self.daily_discrete_dividends.empty + return True +``` diff --git a/EventDriven/configs/base.py b/EventDriven/configs/base.py index 713ca04..2cbdcda 100644 --- a/EventDriven/configs/base.py +++ b/EventDriven/configs/base.py @@ -1,96 +1,18 @@ from trade.helpers.Logging import setup_logger from pydantic.dataclasses import dataclass as pydantic_dataclass from pydantic import ConfigDict, Field -from typing import ClassVar, Literal +from typing import ClassVar +from trade.helpers.helper_types import validate_inputs from weakref import WeakSet -from typing import get_origin, get_args, Union, get_type_hints from EventDriven.exceptions import ( - BacktesterIncorrectTypeError, BacktestConfigAttributeError ) -import types -from dataclasses import fields + from EventDriven.configs.vars import get_class_config_descriptions, get_config_class_description logger = setup_logger(__name__, stream_log_level="WARNING") -def validate_inputs(self): - type_hints = get_type_hints(type(self)) - - for f in fields(self): - try: - field_name = f.name - field_value = getattr(self, field_name) - - type_hint = type_hints.get(field_name) - if type_hint is None: - continue # no annotation, skip - - origin = get_origin(type_hint) - args = get_args(type_hint) - - # --- Handle Literal[...] --- - if origin is Literal: - # e.g. name: Literal["LimitsCog", "OtherCog"] - allowed_values = args # tuple of literals - - if field_value is None: - # If you want to allow None here, add it to the Literal. - logger.warning(f"Configuration '{field_name}' is None but expected one of {allowed_values}.") - elif field_value not in allowed_values: - raise BacktesterIncorrectTypeError( - f"Configuration '{field_name}' expected one of {allowed_values}, " f"but got {field_value!r}." - ) - continue - - # --- Handle Optional / Union[...] --- - if origin in (Union, types.UnionType): - allows_none = any(arg is type(None) for arg in args) - if field_value is None: - if not allows_none: - logger.warning( - f"Configuration '{field_name}' is not set (None) and is not Optional. Please review." - ) - continue - - valid_types = tuple(arg for arg in args if arg is not type(None)) - if not isinstance(field_value, valid_types): - raise BacktesterIncorrectTypeError( - f"Configuration '{field_name}' expected types {valid_types}, " f"but got {type(field_value)}." - ) - continue - - # --- Simple (non-generic) types --- - if origin is None: - if field_value is None: - logger.warning(f"Configuration '{field_name}' is not set (None). Please review.") - continue - - if not isinstance(field_value, type_hint): - raise BacktesterIncorrectTypeError( - f"Configuration '{field_name}' expected type {type_hint}, " f"but got {type(field_value)}." - ) - continue - - # --- Other generics (List, Dict, etc.) – shallow check --- - if field_value is None: - logger.warning(f"Configuration '{field_name}' is not set (None). Please review.") - continue - - try: - if not isinstance(field_value, origin): - raise BacktesterIncorrectTypeError( - f"Configuration '{field_name}' expected type {origin}, " f"but got {type(field_value)}." - ) - except TypeError: - logger.warning( - f"Could not validate field '{field_name}' with value '{field_value}' against type '{type_hint}' due to TypeError." - ) - pass - - except Exception as e: - logger.critical(f"Failed to validate field '{f.name}' in {self.__class__.__name__}. Error: {e}") @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True), kw_only=True) diff --git a/EventDriven/configs/export_configs.py b/EventDriven/configs/export_configs.py index 4f3f03a..0f75719 100644 --- a/EventDriven/configs/export_configs.py +++ b/EventDriven/configs/export_configs.py @@ -107,7 +107,7 @@ def save_to_yaml(self, filename: str): yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False) @classmethod - def load_from_yaml(cls, filename: str) -> 'RunConfigBundle': + def load_from_yaml(cls, filename: str = None, data = None) -> 'RunConfigBundle': """ Load a config bundle from a YAML file. @@ -117,10 +117,13 @@ def load_from_yaml(cls, filename: str) -> 'RunConfigBundle': Returns: RunConfigBundle: The loaded config bundle. """ - with open(filename, "r") as f: - data = yaml.safe_load(f) + if data is None: + assert filename is not None, "Either filename or confs must be provided." + with open(filename, "r") as f: + data = yaml.safe_load(f) confs = data.get('configs', {}) + conf_bund_cls: ConfigsDict = {} # type: ignore for label in confs.keys(): # Extract class name without _1, _2 suffixes diff --git a/EventDriven/dataclasses/states.py b/EventDriven/dataclasses/states.py index 7f0d241..8da93b6 100644 --- a/EventDriven/dataclasses/states.py +++ b/EventDriven/dataclasses/states.py @@ -2,9 +2,9 @@ from pydantic import ConfigDict, Field from typing import Optional, List from datetime import datetime +from trade.datamanager.market_data import AtIndexResult from EventDriven.types import Order from EventDriven.dataclasses.orders import OrderRequest -from EventDriven.riskmanager.market_data import AtIndexResult from EventDriven.dataclasses.timeseries import AtTimePositionData from EventDriven.dataclasses.limits import PositionLimits from EventDriven.riskmanager.actions import RMAction diff --git a/EventDriven/helpers.py b/EventDriven/helpers.py index 253c765..cb5e139 100644 --- a/EventDriven/helpers.py +++ b/EventDriven/helpers.py @@ -1,4 +1,6 @@ import pandas as pd +from typing import Tuple +from trade.helpers.helper import parse_option_tick def generate_signal_id(underlier, date, @@ -28,3 +30,13 @@ def parse_signal_id(id): else: raise ValueError(f'Invalid signal id `{id}`, neither LONG nor SHORT was found in the id') + +def parse_position_id(positionID: str) -> Tuple[dict, list]: + position_str = positionID + position_list = position_str.split("&") + position_list = [x.split(":") for x in position_list if x] + position_list_parsed = [(x[0], parse_option_tick(x[1])) for x in position_list] + position_dict = dict(L=[], S=[]) + for x in position_list_parsed: + position_dict[x[0]].append(x[1]) + return position_dict, position_list \ No newline at end of file diff --git a/EventDriven/riskmanager/.decomm/base.py b/EventDriven/riskmanager/.decomm/base.py index 84758c0..de2b66a 100644 --- a/EventDriven/riskmanager/.decomm/base.py +++ b/EventDriven/riskmanager/.decomm/base.py @@ -1,1601 +1,1601 @@ -## NOTE: -## 1) If a split happens during a backtest window, the trade id won't be updated. The dataframe will simply be uploaded with a the split adjusted strike. -## 2) All Greeks & Midpoint with Zero values will be FFWD'ed -## 3) Do something about all these caches locations. I don't like it. It's confusing - -from pprint import pprint -from .utils import * -from .utils import (logger, - get_timeseries_start_end, - set_deleted_keys, - add_skip_columns, - _clean_data, - PERSISTENT_CACHE, - dynamic_memoize, - get_use_temp_cache, - get_persistent_cache, - load_position_data, - enrich_data, - generate_spot_greeks, - parse_position_id) -from .actions import * -from .picker import * -from .sizer import BaseSizer, DefaultSizer, ZscoreRVolSizer -from .config import ffwd_data -from trade.helpers.helper import printmd, CustomCache, date_inbetween, compare_dates -from EventDriven.event import ( - RollEvent, - ExerciseEvent, - OrderEvent -) -import numpy as np -import os -from cachetools import cached, LRUCache -from EventDriven.execution import ExecutionHandler -from cachetools.keys import hashkey -import time -from cachetools import cachedmethod -from functools import lru_cache -from trade.assets.helpers.utils import (swap_ticker) -from dateutil.relativedelta import relativedelta -import yaml -from ._orders import resolve_schema -from EventDriven.riskmanager.picker.order_picker import OrderPicker -from EventDriven.riskmanager.market_timeseries import BacktestTimeseries - -## IMPORT FROM _vars -BASE = Path(os.environ["WORK_DIR"])/ ".riskmanager_cache" ## Main Cache for RiskManager -HOME_BASE = Path(os.environ["WORK_DIR"])/".cache" -BASE.mkdir(exist_ok=True) - -with open(f'{os.environ["WORK_DIR"]}/EventDriven/riskmanager/config.yaml', 'r') as f: - CONFIG = yaml.safe_load(f) - -order_cache = CustomCache(BASE, fname = "order") - - -def load_riskmanager_cache(): - - """ Load the risk manager cache based on the USE_TEMP_CACHE setting.""" - if get_use_temp_cache(): - logger.info("Using Temporary Cache for RiskManager") - spot_timeseries = CustomCache(BASE/"temp", fname = "rm_spot_timeseries", expire_days=100) - chain_spot_timeseries = CustomCache(BASE/"temp", fname = "rm_chain_spot_timeseries", expire_days=100) ## This is used for pricing, to account option strikes for splits - processed_option_data = CustomCache(BASE/"temp", fname = "rm_processed_option_data", expire_days=100) - position_data = CustomCache(BASE/"temp", fname = "rm_position_data", clear_on_exit=True) - dividend_timeseries = CustomCache(BASE/"temp", fname = "rm_dividend_timeseries", expire_days=100) - else: - spot_timeseries = CustomCache(BASE, fname = "rm_spot_timeseries", expire_days=100) - chain_spot_timeseries = CustomCache(BASE, fname = "rm_chain_spot_timeseries", expire_days=100) ## This is used for pricing, to account option strikes for splits - processed_option_data = CustomCache(BASE, fname = "rm_processed_option_data", expire_days=100) - position_data = CustomCache(BASE, fname = "rm_position_data", clear_on_exit=True) - dividend_timeseries = CustomCache(BASE, fname = "rm_dividend_timeseries", expire_days=100) +# ## NOTE: +# ## 1) If a split happens during a backtest window, the trade id won't be updated. The dataframe will simply be uploaded with a the split adjusted strike. +# ## 2) All Greeks & Midpoint with Zero values will be FFWD'ed +# ## 3) Do something about all these caches locations. I don't like it. It's confusing + +# from pprint import pprint +# from .utils import * +# from .utils import (logger, +# get_timeseries_start_end, +# set_deleted_keys, +# add_skip_columns, +# _clean_data, +# PERSISTENT_CACHE, +# dynamic_memoize, +# get_use_temp_cache, +# get_persistent_cache, +# load_position_data, +# enrich_data, +# generate_spot_greeks, +# parse_position_id) +# from .actions import * +# from .picker import * +# from .sizer import BaseSizer, DefaultSizer, ZscoreRVolSizer +# from .config import ffwd_data +# from trade.helpers.helper import printmd, CustomCache, date_inbetween, compare_dates +# from EventDriven.event import ( +# RollEvent, +# ExerciseEvent, +# OrderEvent +# ) +# import numpy as np +# import os +# from cachetools import cached, LRUCache +# from EventDriven.execution import ExecutionHandler +# from cachetools.keys import hashkey +# import time +# from cachetools import cachedmethod +# from functools import lru_cache +# from trade.assets.helpers.utils import (swap_ticker) +# from dateutil.relativedelta import relativedelta +# import yaml +# from ._orders import resolve_schema +# from EventDriven.riskmanager.picker.order_picker import OrderPicker +# from EventDriven.riskmanager.market_timeseries import BacktestTimeseries + +# ## IMPORT FROM _vars +# BASE = Path(os.environ["WORK_DIR"])/ ".riskmanager_cache" ## Main Cache for RiskManager +# HOME_BASE = Path(os.environ["WORK_DIR"])/".cache" +# BASE.mkdir(exist_ok=True) + +# with open(f'{os.environ["WORK_DIR"]}/EventDriven/riskmanager/config.yaml', 'r') as f: +# CONFIG = yaml.safe_load(f) + +# order_cache = CustomCache(BASE, fname = "order") + + +# def load_riskmanager_cache(): + +# """ Load the risk manager cache based on the USE_TEMP_CACHE setting.""" +# if get_use_temp_cache(): +# logger.info("Using Temporary Cache for RiskManager") +# spot_timeseries = CustomCache(BASE/"temp", fname = "rm_spot_timeseries", expire_days=100) +# chain_spot_timeseries = CustomCache(BASE/"temp", fname = "rm_chain_spot_timeseries", expire_days=100) ## This is used for pricing, to account option strikes for splits +# processed_option_data = CustomCache(BASE/"temp", fname = "rm_processed_option_data", expire_days=100) +# position_data = CustomCache(BASE/"temp", fname = "rm_position_data", clear_on_exit=True) +# dividend_timeseries = CustomCache(BASE/"temp", fname = "rm_dividend_timeseries", expire_days=100) +# else: +# spot_timeseries = CustomCache(BASE, fname = "rm_spot_timeseries", expire_days=100) +# chain_spot_timeseries = CustomCache(BASE, fname = "rm_chain_spot_timeseries", expire_days=100) ## This is used for pricing, to account option strikes for splits +# processed_option_data = CustomCache(BASE, fname = "rm_processed_option_data", expire_days=100) +# position_data = CustomCache(BASE, fname = "rm_position_data", clear_on_exit=True) +# dividend_timeseries = CustomCache(BASE, fname = "rm_dividend_timeseries", expire_days=100) - ## Not dependent on USE_TEMP_CACHE, so always use the persistent cache. - splits_raw =CustomCache(HOME_BASE, fname = "split_names_dates", expire_days = 1000) - special_dividend = CustomCache(HOME_BASE, fname = 'special_dividend', expire_days=1000) ## Special dividend cache for handling special dividends - special_dividend['COST'] = { - '2020-12-01': 10, - '2023-12-27': 15 - } +# ## Not dependent on USE_TEMP_CACHE, so always use the persistent cache. +# splits_raw =CustomCache(HOME_BASE, fname = "split_names_dates", expire_days = 1000) +# special_dividend = CustomCache(HOME_BASE, fname = 'special_dividend', expire_days=1000) ## Special dividend cache for handling special dividends +# special_dividend['COST'] = { +# '2020-12-01': 10, +# '2023-12-27': 15 +# } - return (spot_timeseries, - chain_spot_timeseries, - processed_option_data, - position_data, - dividend_timeseries, - splits_raw, - special_dividend) - - - - -def get_order_cache(): - """ - Returns the order cache - """ - global order_cache - return order_cache - - - - -class RiskManager: - """ - RiskManager class for managing portfolio risk and executing strategies. - - Attributes: - ---------- - Core Attributes: - ---------------- - bars : Bars - The Bars object containing historical price data for the symbols. - events : Events - The Events object used for event-driven backtesting. - initial_capital : float - The initial capital allocated for the portfolio. - start_date : str | datetime - The start date for the backtest, recommended to match the start date of the Bars object. - end_date : str | datetime - The end date for the backtest, recommended to match the end date of the Bars object. - pm_start_date : str | datetime - The start date for the portfolio manager. - pm_end_date : str | datetime - The end date for the portfolio manager. - symbol_list : list[str] - List of symbols available in the Bars object. - OrderPicker : OrderPicker - The OrderPicker object used for selecting orders based on criteria. - - Cache Attributes: - ----------------- - spot_timeseries : CustomCache - Cache for storing the spot price timeseries data. - chain_spot_timeseries : CustomCache - Cache for storing the chain spot price timeseries data, used for pricing and accounting for option strikes during splits. - processed_option_data : CustomCache - Cache for storing processed individual option data. - position_data : CustomCache - Cache for storing position data. - dividend_timeseries : CustomCache - Cache for storing dividend timeseries data. - splits_raw : CustomCache - Cache for storing raw split names and dates. - splits : dict - Processed split data derived from splits_raw. - schema_cache : dict - Cache for storing schema-related data. - _order_cache : dict - Cache for storing order-related data. - id_meta : dict - Metadata for tracking IDs. - _analyzed_date_list : list - List of dates that have been analyzed for actions. - To re-analyze a date, it must be removed from this list. - - Risk Management Attributes: - --------------------------- - sizing_type : str - Specifies the sizing type for calculating quantities (e.g., 'delta', 'vega', 'gamma', 'price'). - sizing_lev : float - Multiplier for equity equivalent size (leverage). Default is 5.0. - limits : dict[str, bool] - Specifies which risk limits are enabled. - greek_limits : dict[str, dict] - Specifies the limits for individual Greeks (e.g., delta, gamma, vega, theta). - max_moneyness : float - Maximum moneyness before rolling positions. Default is 1.2. - max_dte_tolerance : int - Maximum days-to-expiration tolerance for options. Default is 90 days. - moneyness_width : float - Width of moneyness for filtering options. Default is 0.45. - max_slippage : float - Maximum allowable slippage for trades. Default is 0.25. - re_update_on_roll : bool - If True, the limits will be re-evaluated on roll events. Default is False. - - Pricing and Data Attributes: - ---------------------------- - price_on : str - Specifies the price type used for calculations (e.g., 'mkt_close'). Default is 'mkt_close'. - option_price : str - Specifies the option price used for pricing. Default is 'Midpoint'. Available options include: - 'Midpoint', 'Bid', 'Ask', 'Close', 'Weighted Midpoint'. - rf_timeseries : pd.Series - Risk-free rate timeseries data, retrieved using get_risk_free_rate_helper(). - unadjusted_signals : pd.DataFrame - Unadjusted signals for the risk manager, used for analysis and actions. - - Miscellaneous Attributes: - ------------------------- - data_managers : dict - Dictionary for managing data-related objects. - _actions : dict - Internal dictionary for storing actions related to risk management. - executor : ExecutionHandler - The execution handler responsible for executing trades. - t_plus_n : int - Settlement delay for orders (T+N). Default is 0, meaning no settlement delay. - """ - - def __init__(self, - bars: DataHandler, - events: EventScheduler, - initial_capital: int|float, - start_date: str|datetime, - end_date: str|datetime, - executor: ExecutionHandler, - unadjusted_signals: pd.DataFrame, - portfolio_manager: 'Portfolio' = None, - price_on = 'close', - option_price = 'Midpoint', - sizing_type = 'delta', - leverage = 5.0, - max_moneyness = 1.2, - t_plus_n = 0, - **kwargs - ): - """ - Methods: - -------- - __init__(self, bars: Bars, events: Events, initial_capital: float, start_date: str | datetime, end_date: str | datetime, - executor: ExecutionHandler, unadjusted_signals: pd.DataFrame, portfolio_manager: PortfolioManager = None, - price_on: str = 'mkt_close', option_price: str = 'Midpoint', sizing_type: str = 'delta', leverage: float = 5.0, - max_moneyness: float = 1.2, t_plus_n: int = 0, **kwargs): - Initializes the RiskManager class and sets up attributes for managing portfolio risk. - - Parameters: - ---------- - bars : Bars - The Bars object containing historical price data for the symbols. - events : Events - The Events object used for event-driven backtesting. - initial_capital : float - The initial capital allocated for the portfolio. - start_date : str | datetime - The start date for the backtest, recommended to match the start date of the Bars object. - end_date : str | datetime - The end date for the backtest, recommended to match the end date of the Bars object. - executor : ExecutionHandler - The execution handler responsible for executing trades. - unadjusted_signals : pd.DataFrame - Unadjusted signals for the risk manager, used for analysis and actions. - portfolio_manager : PortfolioManager, optional - The PortfolioManager object for managing portfolio positions and orders. Default is None. - price_on : str, optional - Specifies the price type used for calculations (e.g., 'mkt_close'). Default is 'mkt_close'. - option_price : str, optional - Specifies the option price used for pricing. Default is 'Midpoint'. Available options include: - 'Midpoint', 'Bid', 'Ask', 'Close', 'Weighted Midpoint'. - sizing_type : str, optional - Specifies the sizing type for calculating quantities (e.g., 'delta', 'vega', 'gamma', 'price'). Default is 'delta'. - leverage : float, optional - Multiplier for equity equivalent size (leverage). Default is 5.0. - Example: (Cash Available / Spot Price) * Leverage = Equity Equivalent Size. - max_moneyness : float, optional - Maximum moneyness before rolling positions. Default is 1.2. - t_plus_n : int, optional - Settlement delay for orders (T+N). Default is 0, meaning no settlement delay. - **kwargs : dict, optional - Additional keyword arguments for customization. Expected keys include: - - `max_dte_tolerance` (int): Maximum days-to-expiration tolerance for options. Default is 90 days. - - `moneyness_width` (float): Width of moneyness for filtering options. Default is 0.45. - - `max_tries` (int): Maximum number of tries to resolve schema. Default is 20. - """ +# return (spot_timeseries, +# chain_spot_timeseries, +# processed_option_data, +# position_data, +# dividend_timeseries, +# splits_raw, +# special_dividend) + + + + +# def get_order_cache(): +# """ +# Returns the order cache +# """ +# global order_cache +# return order_cache + + + + +# class RiskManager: +# """ +# RiskManager class for managing portfolio risk and executing strategies. + +# Attributes: +# ---------- +# Core Attributes: +# ---------------- +# bars : Bars +# The Bars object containing historical price data for the symbols. +# events : Events +# The Events object used for event-driven backtesting. +# initial_capital : float +# The initial capital allocated for the portfolio. +# start_date : str | datetime +# The start date for the backtest, recommended to match the start date of the Bars object. +# end_date : str | datetime +# The end date for the backtest, recommended to match the end date of the Bars object. +# pm_start_date : str | datetime +# The start date for the portfolio manager. +# pm_end_date : str | datetime +# The end date for the portfolio manager. +# symbol_list : list[str] +# List of symbols available in the Bars object. +# OrderPicker : OrderPicker +# The OrderPicker object used for selecting orders based on criteria. + +# Cache Attributes: +# ----------------- +# spot_timeseries : CustomCache +# Cache for storing the spot price timeseries data. +# chain_spot_timeseries : CustomCache +# Cache for storing the chain spot price timeseries data, used for pricing and accounting for option strikes during splits. +# processed_option_data : CustomCache +# Cache for storing processed individual option data. +# position_data : CustomCache +# Cache for storing position data. +# dividend_timeseries : CustomCache +# Cache for storing dividend timeseries data. +# splits_raw : CustomCache +# Cache for storing raw split names and dates. +# splits : dict +# Processed split data derived from splits_raw. +# schema_cache : dict +# Cache for storing schema-related data. +# _order_cache : dict +# Cache for storing order-related data. +# id_meta : dict +# Metadata for tracking IDs. +# _analyzed_date_list : list +# List of dates that have been analyzed for actions. +# To re-analyze a date, it must be removed from this list. + +# Risk Management Attributes: +# --------------------------- +# sizing_type : str +# Specifies the sizing type for calculating quantities (e.g., 'delta', 'vega', 'gamma', 'price'). +# sizing_lev : float +# Multiplier for equity equivalent size (leverage). Default is 5.0. +# limits : dict[str, bool] +# Specifies which risk limits are enabled. +# greek_limits : dict[str, dict] +# Specifies the limits for individual Greeks (e.g., delta, gamma, vega, theta). +# max_moneyness : float +# Maximum moneyness before rolling positions. Default is 1.2. +# max_dte_tolerance : int +# Maximum days-to-expiration tolerance for options. Default is 90 days. +# moneyness_width : float +# Width of moneyness for filtering options. Default is 0.45. +# max_slippage : float +# Maximum allowable slippage for trades. Default is 0.25. +# re_update_on_roll : bool +# If True, the limits will be re-evaluated on roll events. Default is False. + +# Pricing and Data Attributes: +# ---------------------------- +# price_on : str +# Specifies the price type used for calculations (e.g., 'mkt_close'). Default is 'mkt_close'. +# option_price : str +# Specifies the option price used for pricing. Default is 'Midpoint'. Available options include: +# 'Midpoint', 'Bid', 'Ask', 'Close', 'Weighted Midpoint'. +# rf_timeseries : pd.Series +# Risk-free rate timeseries data, retrieved using get_risk_free_rate_helper(). +# unadjusted_signals : pd.DataFrame +# Unadjusted signals for the risk manager, used for analysis and actions. + +# Miscellaneous Attributes: +# ------------------------- +# data_managers : dict +# Dictionary for managing data-related objects. +# _actions : dict +# Internal dictionary for storing actions related to risk management. +# executor : ExecutionHandler +# The execution handler responsible for executing trades. +# t_plus_n : int +# Settlement delay for orders (T+N). Default is 0, meaning no settlement delay. +# """ + +# def __init__(self, +# bars: DataHandler, +# events: EventScheduler, +# initial_capital: int|float, +# start_date: str|datetime, +# end_date: str|datetime, +# executor: ExecutionHandler, +# unadjusted_signals: pd.DataFrame, +# portfolio_manager: 'Portfolio' = None, +# price_on = 'close', +# option_price = 'Midpoint', +# sizing_type = 'delta', +# leverage = 5.0, +# max_moneyness = 1.2, +# t_plus_n = 0, +# **kwargs +# ): +# """ +# Methods: +# -------- +# __init__(self, bars: Bars, events: Events, initial_capital: float, start_date: str | datetime, end_date: str | datetime, +# executor: ExecutionHandler, unadjusted_signals: pd.DataFrame, portfolio_manager: PortfolioManager = None, +# price_on: str = 'mkt_close', option_price: str = 'Midpoint', sizing_type: str = 'delta', leverage: float = 5.0, +# max_moneyness: float = 1.2, t_plus_n: int = 0, **kwargs): +# Initializes the RiskManager class and sets up attributes for managing portfolio risk. + +# Parameters: +# ---------- +# bars : Bars +# The Bars object containing historical price data for the symbols. +# events : Events +# The Events object used for event-driven backtesting. +# initial_capital : float +# The initial capital allocated for the portfolio. +# start_date : str | datetime +# The start date for the backtest, recommended to match the start date of the Bars object. +# end_date : str | datetime +# The end date for the backtest, recommended to match the end date of the Bars object. +# executor : ExecutionHandler +# The execution handler responsible for executing trades. +# unadjusted_signals : pd.DataFrame +# Unadjusted signals for the risk manager, used for analysis and actions. +# portfolio_manager : PortfolioManager, optional +# The PortfolioManager object for managing portfolio positions and orders. Default is None. +# price_on : str, optional +# Specifies the price type used for calculations (e.g., 'mkt_close'). Default is 'mkt_close'. +# option_price : str, optional +# Specifies the option price used for pricing. Default is 'Midpoint'. Available options include: +# 'Midpoint', 'Bid', 'Ask', 'Close', 'Weighted Midpoint'. +# sizing_type : str, optional +# Specifies the sizing type for calculating quantities (e.g., 'delta', 'vega', 'gamma', 'price'). Default is 'delta'. +# leverage : float, optional +# Multiplier for equity equivalent size (leverage). Default is 5.0. +# Example: (Cash Available / Spot Price) * Leverage = Equity Equivalent Size. +# max_moneyness : float, optional +# Maximum moneyness before rolling positions. Default is 1.2. +# t_plus_n : int, optional +# Settlement delay for orders (T+N). Default is 0, meaning no settlement delay. +# **kwargs : dict, optional +# Additional keyword arguments for customization. Expected keys include: +# - `max_dte_tolerance` (int): Maximum days-to-expiration tolerance for options. Default is 90 days. +# - `moneyness_width` (float): Width of moneyness for filtering options. Default is 0.45. +# - `max_tries` (int): Maximum number of tries to resolve schema. Default is 20. +# """ - assert sizing_type in ['delta', 'vega', 'gamma', 'price'], f"Sizing Type {sizing_type} not recognized, expected 'delta', 'vega', 'gamma', or 'price'" - order_cache.clear() - global DELETED_KEYS - set_deleted_keys([]) ## Set the deletion keys for the cache - start, end = get_timeseries_start_end() - self.bars = bars - self.events = events - self.initial_capital = initial_capital - self.__pm = portfolio_manager - self.start_date = start - self.pm_start_date = start_date - self.pm_end_date = end_date - self.end_date = end - self.symbol_list = self.bars.symbol_list - self.order_picker = OrderPicker(start, end) +# assert sizing_type in ['delta', 'vega', 'gamma', 'price'], f"Sizing Type {sizing_type} not recognized, expected 'delta', 'vega', 'gamma', or 'price'" +# order_cache.clear() +# global DELETED_KEYS +# set_deleted_keys([]) ## Set the deletion keys for the cache +# start, end = get_timeseries_start_end() +# self.bars = bars +# self.events = events +# self.initial_capital = initial_capital +# self.__pm = portfolio_manager +# self.start_date = start +# self.pm_start_date = start_date +# self.pm_end_date = end_date +# self.end_date = end +# self.symbol_list = self.bars.symbol_list +# self.order_picker = OrderPicker(start, end) - ## Load data caches. USE_TEMP_CACHE == True means a reset every kernel refresh. Else persists over days. - ( - self.spot_timeseries, - self.chain_spot_timeseries, - self.processed_option_data, - self.position_data, - self.dividend_timeseries, - self.splits_raw, - self.special_dividends - ) = load_riskmanager_cache() - self.sizing_type = sizing_type - self.sizing_lev = leverage - self.limits = { - 'delta': True, - 'gamma': False, - 'vega': False, - 'theta': False, - 'dte': False, - 'moneyness': False - } - self.greek_limits = { - 'delta': {}, - 'gamma': {}, - 'vega': {}, - 'theta': {} - } - self.data_managers = {} - - ## Might want to make this changeable in future - self.rf_timeseries = get_risk_free_rate_helper()['annualized'] - self.price_on = price_on - self.max_moneyness = max_moneyness - self.option_price = option_price - self._actions = {} - self.splits = self.set_splits(self.splits_raw) - self.schema_cache = {} - self.max_dte_tolerance = kwargs.get('max_dte_tolerance', 90) ## Default is 90 days - self.otm_moneyness_width = kwargs.get('moneyness_width', 0.45) ## Default is 0.45 - self.itm_moneyness_width = kwargs.get('itm_moneyness_width', 0.45) ## Default is 0.45 - self.max_tries = kwargs.get('max_tries', 20) ## Default is 20 tries to resolve schema - self.__analyzed_date_list = [] ## List of dates that have been analyzed for actions - self._order_cache = {} - self.id_meta = {} - self.t_plus_n = t_plus_n ## T+N settlement for the orders, default is 0, meaning no settlement delay. Orders will be placed on the same day. - self.max_slippage = 0.25 - self.min_slippage = 0.16 - self.executor = executor - self.re_update_on_roll = False ## If True, the limits will be re-evaluated on roll events. Default is False - self.unadjusted_signals = unadjusted_signals ## Unadjusted signals for the risk manager, used for analysis and actions - self.__sizer = None - self.add_columns = [] - self.skip_adj_count = 0 ## Counter for skipped adjustments, used to skip adjustments for a certain number of times. - self.limits_meta = {} - self.market_data = BacktestTimeseries(_start=self.start_date, _end=self.end_date) - - @property - def option_data(self): - global close_cache - return close_cache +# ## Load data caches. USE_TEMP_CACHE == True means a reset every kernel refresh. Else persists over days. +# ( +# self.spot_timeseries, +# self.chain_spot_timeseries, +# self.processed_option_data, +# self.position_data, +# self.dividend_timeseries, +# self.splits_raw, +# self.special_dividends +# ) = load_riskmanager_cache() +# self.sizing_type = sizing_type +# self.sizing_lev = leverage +# self.limits = { +# 'delta': True, +# 'gamma': False, +# 'vega': False, +# 'theta': False, +# 'dte': False, +# 'moneyness': False +# } +# self.greek_limits = { +# 'delta': {}, +# 'gamma': {}, +# 'vega': {}, +# 'theta': {} +# } +# self.data_managers = {} + +# ## Might want to make this changeable in future +# self.rf_timeseries = get_risk_free_rate_helper()['annualized'] +# self.price_on = price_on +# self.max_moneyness = max_moneyness +# self.option_price = option_price +# self._actions = {} +# self.splits = self.set_splits(self.splits_raw) +# self.schema_cache = {} +# self.max_dte_tolerance = kwargs.get('max_dte_tolerance', 90) ## Default is 90 days +# self.otm_moneyness_width = kwargs.get('moneyness_width', 0.45) ## Default is 0.45 +# self.itm_moneyness_width = kwargs.get('itm_moneyness_width', 0.45) ## Default is 0.45 +# self.max_tries = kwargs.get('max_tries', 20) ## Default is 20 tries to resolve schema +# self.__analyzed_date_list = [] ## List of dates that have been analyzed for actions +# self._order_cache = {} +# self.id_meta = {} +# self.t_plus_n = t_plus_n ## T+N settlement for the orders, default is 0, meaning no settlement delay. Orders will be placed on the same day. +# self.max_slippage = 0.25 +# self.min_slippage = 0.16 +# self.executor = executor +# self.re_update_on_roll = False ## If True, the limits will be re-evaluated on roll events. Default is False +# self.unadjusted_signals = unadjusted_signals ## Unadjusted signals for the risk manager, used for analysis and actions +# self.__sizer = None +# self.add_columns = [] +# self.skip_adj_count = 0 ## Counter for skipped adjustments, used to skip adjustments for a certain number of times. +# self.limits_meta = {} +# self.market_data = BacktestTimeseries(_start=self.start_date, _end=self.end_date) + +# @property +# def option_data(self): +# global close_cache +# return close_cache - @property - def order_cache(self): - """ - Returns the order cache - """ - return self._order_cache +# @property +# def order_cache(self): +# """ +# Returns the order cache +# """ +# return self._order_cache - @property - def sizer(self): - """ - Getter for the sizer - """ - if isinstance(self.__sizer, (BaseSizer, DefaultSizer, ZscoreRVolSizer)): - return self.__sizer - elif self.__sizer is None: - self.__sizer = DefaultSizer(pm=self.__pm, rm=self, sizing_lev=self.sizing_lev) - return self.__sizer - else: - raise TypeError("Sizer must be an instance of BaseSizer or its subclasses. Reset with None to use DefaultSizer.") - - @sizer.setter - def sizer(self, value): - """ - Setter for the sizer - """ - if isinstance(value, (BaseSizer, DefaultSizer, ZscoreRVolSizer)) or value is None: - self.__sizer = value - else: - raise TypeError("Sizer must be an instance of BaseSizer or its subclasses.") +# @property +# def sizer(self): +# """ +# Getter for the sizer +# """ +# if isinstance(self.__sizer, (BaseSizer, DefaultSizer, ZscoreRVolSizer)): +# return self.__sizer +# elif self.__sizer is None: +# self.__sizer = DefaultSizer(pm=self.__pm, rm=self, sizing_lev=self.sizing_lev) +# return self.__sizer +# else: +# raise TypeError("Sizer must be an instance of BaseSizer or its subclasses. Reset with None to use DefaultSizer.") + +# @sizer.setter +# def sizer(self, value): +# """ +# Setter for the sizer +# """ +# if isinstance(value, (BaseSizer, DefaultSizer, ZscoreRVolSizer)) or value is None: +# self.__sizer = value +# else: +# raise TypeError("Sizer must be an instance of BaseSizer or its subclasses.") - def clear_caches(self): - """ - Clears the spot, chain_spot, dividend caches - """ - self.spot_timeseries.clear() - self.chain_spot_timeseries.clear() - # self.position_data.clear() - self.dividend_timeseries.clear() - - def clear_core_data_caches(self): - """ - Clears the core data caches used by the RiskManager for a new run. - spot, chain_spot, processed_option_data, position_data - This only clears if USE_TEMP_CACHE is True else nothing happens - """ - if get_use_temp_cache(): - self.spot_timeseries.clear() - self.chain_spot_timeseries.clear() - self.processed_option_data.clear() - self.position_data.clear() - self.dividend_timeseries.clear() - get_persistent_cache().clear() ## Ensures any caching with `.memoize` is cleared as well. - else: - logger.critical("USE_TEMP_CACHE set to False. Cache will not be cleared") +# def clear_caches(self): +# """ +# Clears the spot, chain_spot, dividend caches +# """ +# self.spot_timeseries.clear() +# self.chain_spot_timeseries.clear() +# # self.position_data.clear() +# self.dividend_timeseries.clear() + +# def clear_core_data_caches(self): +# """ +# Clears the core data caches used by the RiskManager for a new run. +# spot, chain_spot, processed_option_data, position_data +# This only clears if USE_TEMP_CACHE is True else nothing happens +# """ +# if get_use_temp_cache(): +# self.spot_timeseries.clear() +# self.chain_spot_timeseries.clear() +# self.processed_option_data.clear() +# self.position_data.clear() +# self.dividend_timeseries.clear() +# get_persistent_cache().clear() ## Ensures any caching with `.memoize` is cleared as well. +# else: +# logger.critical("USE_TEMP_CACHE set to False. Cache will not be cleared") - @property - def pm(self): - return self.__pm +# @property +# def pm(self): +# return self.__pm - @pm.setter - def pm(self, value): - self.__pm = value - - @property - def actions(self): - return pd.DataFrame(self._actions).T - - def submit_add_columns(self, columns: Tuple[str, str]): - """ - Submits additional columns to be added to the risk manager's data. - Args: - columns (Tuple[str, str]): A tuple containing the column names to be added. - Raises: - AssertionError: If the first column is not one of the recognized columns or if the second column is not in the ADD_COLUMNS_FACTORY. - """ - assert isinstance(columns, tuple) and len(columns) == 2, "columns must be a tuple of two strings" - assert columns[0] in ['Midpoint', 'Delta', 'Open', 'Close', 'Closeask', 'Closebid'], f"Column {columns[0]} not recognized, expected 'Midpoint', 'Delta', 'Open', 'Close', 'Closeask', or 'Closebid'" - assert columns[1] in ADD_COLUMNS_FACTORY.keys(), f"Column {columns[1]} not recognized, expected {ADD_COLUMNS_FACTORY.keys()}" - ## Check if the columns already exist in the add_columns list - if columns in self.add_columns: - logger.info(f"Column {columns} already exists in add_columns, skipping addition") - return - self.add_columns.append(columns) - - def set_splits(self, d): - """ - Setter for splits - """ - splits_dict = {} - for k, v in d.items(): - splits_dict[k] = [] - for d in v: - if date_inbetween(d[0], self.start_date, self.end_date): - splits_dict[k].append(d) - return splits_dict - - - def print_settings(self): - msg = f""" -Risk Manager Settings: -Start Date: {self.pm_start_date} -End Date: {self.pm_end_date} -Current Limits State (Position Adjusted when these thresholds are reached): - Delta: {self.limits['delta']} - Gamma: {self.limits['gamma']} - Vega: {self.limits['vega']} - Theta: {self.limits['theta']} - Roll On DTE: {self.limits['dte']} - Min DTE Threshold: {self.pm.min_acceptable_dte_threshold} - Roll On Moneyness: {self.limits['moneyness']} - Max Moneyness: {self.max_moneyness} -Quanitity Sizing Type: {self.sizing_type} - """ - print(msg) +# @pm.setter +# def pm(self, value): +# self.__pm = value + +# @property +# def actions(self): +# return pd.DataFrame(self._actions).T + +# def submit_add_columns(self, columns: Tuple[str, str]): +# """ +# Submits additional columns to be added to the risk manager's data. +# Args: +# columns (Tuple[str, str]): A tuple containing the column names to be added. +# Raises: +# AssertionError: If the first column is not one of the recognized columns or if the second column is not in the ADD_COLUMNS_FACTORY. +# """ +# assert isinstance(columns, tuple) and len(columns) == 2, "columns must be a tuple of two strings" +# assert columns[0] in ['Midpoint', 'Delta', 'Open', 'Close', 'Closeask', 'Closebid'], f"Column {columns[0]} not recognized, expected 'Midpoint', 'Delta', 'Open', 'Close', 'Closeask', or 'Closebid'" +# assert columns[1] in ADD_COLUMNS_FACTORY.keys(), f"Column {columns[1]} not recognized, expected {ADD_COLUMNS_FACTORY.keys()}" +# ## Check if the columns already exist in the add_columns list +# if columns in self.add_columns: +# logger.info(f"Column {columns} already exists in add_columns, skipping addition") +# return +# self.add_columns.append(columns) + +# def set_splits(self, d): +# """ +# Setter for splits +# """ +# splits_dict = {} +# for k, v in d.items(): +# splits_dict[k] = [] +# for d in v: +# if date_inbetween(d[0], self.start_date, self.end_date): +# splits_dict[k].append(d) +# return splits_dict + + +# def print_settings(self): +# msg = f""" +# Risk Manager Settings: +# Start Date: {self.pm_start_date} +# End Date: {self.pm_end_date} +# Current Limits State (Position Adjusted when these thresholds are reached): +# Delta: {self.limits['delta']} +# Gamma: {self.limits['gamma']} +# Vega: {self.limits['vega']} +# Theta: {self.limits['theta']} +# Roll On DTE: {self.limits['dte']} +# Min DTE Threshold: {self.pm.min_acceptable_dte_threshold} +# Roll On Moneyness: {self.limits['moneyness']} +# Max Moneyness: {self.max_moneyness} +# Quanitity Sizing Type: {self.sizing_type} +# """ +# print(msg) - # @log_error_with_stack(logger) - # @log_time(time_logger) - def get_order(self, *args, **kwargs): - """ - Compulsory variables for OrderSchema: - signal_id: str: Unique identifier for the signal - date: str|datetime: Date for which the order is to be placed - tick: str: Ticker for the option contract - max_close: float: Maximum close price for the order - strategy: str: Strategy type - option_type: str: Option type - target_dte: int: Target days to expiration - structure_direction: str: Direction of the structure +# # @log_error_with_stack(logger) +# # @log_time(time_logger) +# def get_order(self, *args, **kwargs): +# """ +# Compulsory variables for OrderSchema: +# signal_id: str: Unique identifier for the signal +# date: str|datetime: Date for which the order is to be placed +# tick: str: Ticker for the option contract +# max_close: float: Maximum close price for the order +# strategy: str: Strategy type +# option_type: str: Option type +# target_dte: int: Target days to expiration +# structure_direction: str: Direction of the structure - Optional variables: - spread_ticks: int: Number of ticks for the spread, default is 1 - dte_tolerance: int: Tolerance for days to expiration, default is 60 - min_moneyness: float: Minimum moneyness for the order, default is 0.75 - max_moneyness: float: Maximum moneyness for the order, default is 1.25 - min_total_price: float: Minimum total price for the order, default is max_close/2 - - This function generates an order based on the provided parameters and returns it. - """ - ## Initialize the order cache if it doesn't exist - order_cache = self.order_cache - signalID = kwargs.pop('signal_id') - date = kwargs.get('date') - tick = kwargs.get('tick') - max_close = kwargs.get('max_close', 2.0) - option_strategy = kwargs.pop('strategy') - option_type = kwargs.pop('option_type') - structure_direction = kwargs.pop('structure_direction') - spread_ticks = kwargs.pop('spread_ticks', 1) - dte_tolerance = kwargs.pop('dte_tolerance', 60) - min_moneyness = kwargs.pop('min_moneyness', 0.75) - max_moneyness = kwargs.pop('max_moneyness', 1.25) - target_dte = kwargs.pop('target_dte') - min_total_price = kwargs.pop('min_total_price', max_close/2) - direction='LONG' if option_type == 'C' else 'SHORT' +# Optional variables: +# spread_ticks: int: Number of ticks for the spread, default is 1 +# dte_tolerance: int: Tolerance for days to expiration, default is 60 +# min_moneyness: float: Minimum moneyness for the order, default is 0.75 +# max_moneyness: float: Maximum moneyness for the order, default is 1.25 +# min_total_price: float: Minimum total price for the order, default is max_close/2 + +# This function generates an order based on the provided parameters and returns it. +# """ +# ## Initialize the order cache if it doesn't exist +# order_cache = self.order_cache +# signalID = kwargs.pop('signal_id') +# date = kwargs.get('date') +# tick = kwargs.get('tick') +# max_close = kwargs.get('max_close', 2.0) +# option_strategy = kwargs.pop('strategy') +# option_type = kwargs.pop('option_type') +# structure_direction = kwargs.pop('structure_direction') +# spread_ticks = kwargs.pop('spread_ticks', 1) +# dte_tolerance = kwargs.pop('dte_tolerance', 60) +# min_moneyness = kwargs.pop('min_moneyness', 0.75) +# max_moneyness = kwargs.pop('max_moneyness', 1.25) +# target_dte = kwargs.pop('target_dte') +# min_total_price = kwargs.pop('min_total_price', max_close/2) +# direction='LONG' if option_type == 'C' else 'SHORT' - if is_USholiday(date): - logger.info(f"Date {date} is a US Holiday, skipping order generation") - return { - 'result': ResultsEnum.IS_HOLIDAY.value, - 'data': None - } - - - self.generate_data(tick) - spot = self.chain_spot_timeseries[tick][date] - - logger.info(f"## ***Signal ID: {signalID}***") - - ## I cannot calculate greeks here. I need option_data to be available first. - # order = self.OrderPicker.get_order(*args, **kwargs) - - ## Testing new order picker - schema = OrderSchema({ - "strategy": option_strategy, "option_type": option_type, "tick": tick, - "target_dte": target_dte, "dte_tolerance": dte_tolerance, - "structure_direction": structure_direction, "max_total_price": max_close, - "spread_ticks":spread_ticks, "min_moneyness": min_moneyness, "max_moneyness": max_moneyness, "increment": 0.5, - "min_total_price": min_total_price - }) - logger.info(f"Initial Schema on {date}: {schema}") - order = self.OrderPicker.get_order_new(schema, date, spot, print_url = False) - - ## Resolve the schema if the order is not successful - tries = 0 - while order['result'] != ResultsEnum.SUCCESSFUL.value: - logger.info(f"Failed to produce order with schema: {schema}, trying to resolve schema, on try {tries}") - pack = resolve_schema(schema, - tries = tries, - max_dte_tolerance = self.max_dte_tolerance, - max_close = self.pm.allocated_cash_map[tick]/100, - max_tries = self.max_tries, - otm_moneyness_width = self.otm_moneyness_width, - itm_moneyness_width = self.itm_moneyness_width) - schema, tries = pack - - if schema is False: - logger.info(f"Unable to resolve schema after {tries} tries, returning None") - self.schema_cache.setdefault(date, {}).update({signalID: schema}) - return { - 'result': ResultsEnum.NO_CONTRACTS_FOUND.value, - 'data': None - } - logger.info(f"Resolved Schema: {schema}, tries: {tries}") - order = self.OrderPicker.get_order_new(schema, date, spot, print_url = False) ## Get the order from the OrderPicker +# if is_USholiday(date): +# logger.info(f"Date {date} is a US Holiday, skipping order generation") +# return { +# 'result': ResultsEnum.IS_HOLIDAY.value, +# 'data': None +# } + + +# self.generate_data(tick) +# spot = self.chain_spot_timeseries[tick][date] + +# logger.info(f"## ***Signal ID: {signalID}***") + +# ## I cannot calculate greeks here. I need option_data to be available first. +# # order = self.OrderPicker.get_order(*args, **kwargs) + +# ## Testing new order picker +# schema = OrderSchema({ +# "strategy": option_strategy, "option_type": option_type, "tick": tick, +# "target_dte": target_dte, "dte_tolerance": dte_tolerance, +# "structure_direction": structure_direction, "max_total_price": max_close, +# "spread_ticks":spread_ticks, "min_moneyness": min_moneyness, "max_moneyness": max_moneyness, "increment": 0.5, +# "min_total_price": min_total_price +# }) +# logger.info(f"Initial Schema on {date}: {schema}") +# order = self.OrderPicker.get_order_new(schema, date, spot, print_url = False) + +# ## Resolve the schema if the order is not successful +# tries = 0 +# while order['result'] != ResultsEnum.SUCCESSFUL.value: +# logger.info(f"Failed to produce order with schema: {schema}, trying to resolve schema, on try {tries}") +# pack = resolve_schema(schema, +# tries = tries, +# max_dte_tolerance = self.max_dte_tolerance, +# max_close = self.pm.allocated_cash_map[tick]/100, +# max_tries = self.max_tries, +# otm_moneyness_width = self.otm_moneyness_width, +# itm_moneyness_width = self.itm_moneyness_width) +# schema, tries = pack + +# if schema is False: +# logger.info(f"Unable to resolve schema after {tries} tries, returning None") +# self.schema_cache.setdefault(date, {}).update({signalID: schema}) +# return { +# 'result': ResultsEnum.NO_CONTRACTS_FOUND.value, +# 'data': None +# } +# logger.info(f"Resolved Schema: {schema}, tries: {tries}") +# order = self.OrderPicker.get_order_new(schema, date, spot, print_url = False) ## Get the order from the OrderPicker - self.schema_cache.setdefault(date, {}).update({signalID: schema}) ## Update the schema cache with the date and signalID +# self.schema_cache.setdefault(date, {}).update({signalID: schema}) ## Update the schema cache with the date and signalID - signal_meta = parse_signal_id(signalID) - logger.info(f"Order Produced: {order}") +# signal_meta = parse_signal_id(signalID) +# logger.info(f"Order Produced: {order}") - if order['result'] == ResultsEnum.SUCCESSFUL.value: - print(f"\nOrder Received: {order}\n") - position_id = order['data']['trade_id'] - order['signal_id'] = signalID - order['direction'] = direction +# if order['result'] == ResultsEnum.SUCCESSFUL.value: +# print(f"\nOrder Received: {order}\n") +# position_id = order['data']['trade_id'] +# order['signal_id'] = signalID +# order['direction'] = direction - else: - print(f"\nOrder Failed: {order}\n") - logger.info(f"Signal ID: {signalID}, Unable to produce order, returning None") - return order +# else: +# print(f"\nOrder Failed: {order}\n") +# logger.info(f"Signal ID: {signalID}, Unable to produce order, returning None") +# return order - logger.info(f"Position ID: {position_id}") - logger.info("Calculating Position Greeks") - self.calculate_position_greeks(position_id, kwargs['date']) - order = self.update_order_close(position_id, kwargs['date'], order) ## Update the order with the close price from the position data - logger.info('Updating Signal Limits') - self.sizer.update_delta_limit(signalID, position_id, date) - - ## Hack to get limit meta - self.store_limits_meta(signalID, position_id, date) - logger.info("Calculating Quantity") - quantity = self.sizer.calculate_position_size(signalID, position_id, order['data']['close'], kwargs['date']) - logger.info(f"Quantity for Position ({position_id}) Date {kwargs['date']}, Signal ID {signalID} is {quantity}") - order['data']['quantity'] = quantity - order['data']['cash_equivalent_qty'] = self.pm.allocated_cash_map[tick] // (order['data']['close'] * 100) - logger.info(order) - - ## save the order in the cache - if date not in order_cache: - cache_dict = {tick: order} - order_cache[date] = cache_dict - else: - cache_dict = order_cache[date] - cache_dict[tick] = order - order_cache[date] = cache_dict +# logger.info(f"Position ID: {position_id}") +# logger.info("Calculating Position Greeks") +# self.calculate_position_greeks(position_id, kwargs['date']) +# order = self.update_order_close(position_id, kwargs['date'], order) ## Update the order with the close price from the position data +# logger.info('Updating Signal Limits') +# self.sizer.update_delta_limit(signalID, position_id, date) + +# ## Hack to get limit meta +# self.store_limits_meta(signalID, position_id, date) +# logger.info("Calculating Quantity") +# quantity = self.sizer.calculate_position_size(signalID, position_id, order['data']['close'], kwargs['date']) +# logger.info(f"Quantity for Position ({position_id}) Date {kwargs['date']}, Signal ID {signalID} is {quantity}") +# order['data']['quantity'] = quantity +# order['data']['cash_equivalent_qty'] = self.pm.allocated_cash_map[tick] // (order['data']['close'] * 100) +# logger.info(order) + +# ## save the order in the cache +# if date not in order_cache: +# cache_dict = {tick: order} +# order_cache[date] = cache_dict +# else: +# cache_dict = order_cache[date] +# cache_dict[tick] = order +# order_cache[date] = cache_dict - self.adjust_slippage(position_id, date) ## Adjust the slippage for the position based on the position data - - return order - - def store_limits_meta(self, signal_id, position_id, date): - """ - Stores the limits meta for the signal and position - """ - - delta = self.greek_limits['delta'].get(signal_id, None) - gamma = self.greek_limits['gamma'].get(signal_id, None) - vega = self.greek_limits['vega'].get(signal_id, None) - theta = self.greek_limits['theta'].get(signal_id, None) - - self.limits_meta[(signal_id, position_id, date)] = { - 'delta': delta, - 'gamma': gamma, - 'vega': vega, - 'theta': theta, - } +# self.adjust_slippage(position_id, date) ## Adjust the slippage for the position based on the position data + +# return order + +# def store_limits_meta(self, signal_id, position_id, date): +# """ +# Stores the limits meta for the signal and position +# """ + +# delta = self.greek_limits['delta'].get(signal_id, None) +# gamma = self.greek_limits['gamma'].get(signal_id, None) +# vega = self.greek_limits['vega'].get(signal_id, None) +# theta = self.greek_limits['theta'].get(signal_id, None) + +# self.limits_meta[(signal_id, position_id, date)] = { +# 'delta': delta, +# 'gamma': gamma, +# 'vega': vega, +# 'theta': theta, +# } - def adjust_slippage(self, position_id, date): - position_data = self.position_data.get(position_id, None) - if position_data is None: - logger.error(f"Position Data for {position_id} not available, cannot adjust slippage") - return None +# def adjust_slippage(self, position_id, date): +# position_data = self.position_data.get(position_id, None) +# if position_data is None: +# logger.error(f"Position Data for {position_id} not available, cannot adjust slippage") +# return None - if 'spread_ratio' in position_data: - spread_ratio = position_data['spread_ratio'][date] if position_data['spread_ratio'][date] else self.max_slippage - decided_slippage = min(spread_ratio, self.max_slippage) - logger.info(f"Position {position_id} on date {date} has spread ratio {spread_ratio}, adjusting slippage to {decided_slippage}") - self.executor.max_slippage_pct = decided_slippage - else: - logger.warning(f"Spread Ratio not available for position {position_id}, using default max slippage of {self.max_slippage}") - self.executor.max_slippage_pct = self.max_slippage - - ## Overriding to risk_manager set for now - self.executor.min_slippage_pct = self.min_slippage - self.executor.max_slippage_pct = self.max_slippage +# if 'spread_ratio' in position_data: +# spread_ratio = position_data['spread_ratio'][date] if position_data['spread_ratio'][date] else self.max_slippage +# decided_slippage = min(spread_ratio, self.max_slippage) +# logger.info(f"Position {position_id} on date {date} has spread ratio {spread_ratio}, adjusting slippage to {decided_slippage}") +# self.executor.max_slippage_pct = decided_slippage +# else: +# logger.warning(f"Spread Ratio not available for position {position_id}, using default max slippage of {self.max_slippage}") +# self.executor.max_slippage_pct = self.max_slippage + +# ## Overriding to risk_manager set for now +# self.executor.min_slippage_pct = self.min_slippage +# self.executor.max_slippage_pct = self.max_slippage - def update_order_close(self, position_id:str, date:str|datetime, order:dict)-> dict: - """ - Updates the close price of the order based on the position data. - Parameters: - position_id: str: ID of the position - date: str|datetime: Date for which the order is to be updated - order: dict: Order dictionary containing the order details - Returns: - dict: Updated order dictionary with the close price - """ - - skip = self.position_data[position_id]['Midpoint_skip_day'][date] - if skip: - self.skip_adj_count += 1 +# def update_order_close(self, position_id:str, date:str|datetime, order:dict)-> dict: +# """ +# Updates the close price of the order based on the position data. +# Parameters: +# position_id: str: ID of the position +# date: str|datetime: Date for which the order is to be updated +# order: dict: Order dictionary containing the order details +# Returns: +# dict: Updated order dictionary with the close price +# """ + +# skip = self.position_data[position_id]['Midpoint_skip_day'][date] +# if skip: +# self.skip_adj_count += 1 - close = self.position_data[position_id]['Midpoint'].ewm(span = 3).mean()[date] if self.option_price == 'Midpoint' \ - else self.position_data[position_id][self.option_price.capitalize()][date] - logger.info(f"***TESTING WITH EWM PRICE*** Position ID: {position_id}, Date: {date} - Skipping Day, using EWM Price") - logger.info(f"OLD CLOSE: {order['data']['close']}, NEW CLOSE: {close}, PCT_CHANGE: {(close - order['data']['close'])/order['data']['close']*100:.2f}%") - else: - close = self.position_data[position_id][self.option_price.capitalize()][date] - order['data']['close'] = close - return order +# close = self.position_data[position_id]['Midpoint'].ewm(span = 3).mean()[date] if self.option_price == 'Midpoint' \ +# else self.position_data[position_id][self.option_price.capitalize()][date] +# logger.info(f"***TESTING WITH EWM PRICE*** Position ID: {position_id}, Date: {date} - Skipping Day, using EWM Price") +# logger.info(f"OLD CLOSE: {order['data']['close']}, NEW CLOSE: {close}, PCT_CHANGE: {(close - order['data']['close'])/order['data']['close']*100:.2f}%") +# else: +# close = self.position_data[position_id][self.option_price.capitalize()][date] +# order['data']['close'] = close +# return order - def register_option_meta_frame( - self, - date: str|datetime, - trade_id:str, - ) -> None: +# def register_option_meta_frame( +# self, +# date: str|datetime, +# trade_id:str, +# ) -> None: - ## Generate a DataFrame for each direction in the trade - trade_meta = self.parse_position_id(trade_id)[0] - direction_pair = self.id_meta.setdefault(trade_id, {'L': pd.DataFrame( - index = bus_range(self.pm_start_date, self.pm_end_date, freq='1d'), - ), 'S': pd.DataFrame( - index = bus_range(self.pm_start_date, self.pm_end_date, freq='1d'), - )}) - - ## Get split info - splits = self.splits - - ## Loop through each direction - for direction, details in trade_meta.items(): - direction_frame = direction_pair.get(direction, pd.DataFrame()) - - ## Loop through each option in the direction - for i, option in enumerate(details): - - ## First populate the Given Option Detail - direction_frame[i] = generate_option_tick_new(*option.values()) - tick = option['ticker'] - split_info = splits.get(tick, None) - if split_info is None: - continue - - ## If there is split info, we adjust - for split_meta in split_info: - if not compare_dates.is_after(split_meta[0], date): - continue - split_start, split_ratio = split_meta - new_details = deepcopy(option) - new_details['strike']/=split_meta[1] - direction_frame.loc[split_start:, i] = generate_option_tick_new(*new_details.values()) - - - # @log_time(time_logger) - # def calculate_position_greeks(self, positionID, date): - # """ - # Calculate the greeks of a position - - # date: Evaluation Date for the greeks (PS: This is not the pricing date) - # positionID: str: position string. (PS: This function assumes ticker for position is the same) - # """ - # logger.info(f"Calculate Greeks Dates Start: {self.start_date}, End: {self.end_date}, Position ID: {positionID}, Date: {date}") - # if positionID in self.position_data: - # ## If the position data is already available, then we can skip this step - # logger.info(f"Position Data for {positionID} already available, skipping calculation") - # return self.position_data[positionID] - # else: - # logger.critical(f"Position Data for {positionID} not available, calculating greeks. Load time ~2 minutes") - # ## Initialize the Long and Short Lists - # long = [] - # short = [] - # threads = [] - # thread_input_list = [ - # [], [], [], [], [], [] - # ] - - # date = pd.to_datetime(date) ## Ensure date is in datetime format +# ## Generate a DataFrame for each direction in the trade +# trade_meta = self.parse_position_id(trade_id)[0] +# direction_pair = self.id_meta.setdefault(trade_id, {'L': pd.DataFrame( +# index = bus_range(self.pm_start_date, self.pm_end_date, freq='1d'), +# ), 'S': pd.DataFrame( +# index = bus_range(self.pm_start_date, self.pm_end_date, freq='1d'), +# )}) + +# ## Get split info +# splits = self.splits + +# ## Loop through each direction +# for direction, details in trade_meta.items(): +# direction_frame = direction_pair.get(direction, pd.DataFrame()) + +# ## Loop through each option in the direction +# for i, option in enumerate(details): + +# ## First populate the Given Option Detail +# direction_frame[i] = generate_option_tick_new(*option.values()) +# tick = option['ticker'] +# split_info = splits.get(tick, None) +# if split_info is None: +# continue + +# ## If there is split info, we adjust +# for split_meta in split_info: +# if not compare_dates.is_after(split_meta[0], date): +# continue +# split_start, split_ratio = split_meta +# new_details = deepcopy(option) +# new_details['strike']/=split_meta[1] +# direction_frame.loc[split_start:, i] = generate_option_tick_new(*new_details.values()) + + +# # @log_time(time_logger) +# # def calculate_position_greeks(self, positionID, date): +# # """ +# # Calculate the greeks of a position + +# # date: Evaluation Date for the greeks (PS: This is not the pricing date) +# # positionID: str: position string. (PS: This function assumes ticker for position is the same) +# # """ +# # logger.info(f"Calculate Greeks Dates Start: {self.start_date}, End: {self.end_date}, Position ID: {positionID}, Date: {date}") +# # if positionID in self.position_data: +# # ## If the position data is already available, then we can skip this step +# # logger.info(f"Position Data for {positionID} already available, skipping calculation") +# # return self.position_data[positionID] +# # else: +# # logger.critical(f"Position Data for {positionID} not available, calculating greeks. Load time ~2 minutes") +# # ## Initialize the Long and Short Lists +# # long = [] +# # short = [] +# # threads = [] +# # thread_input_list = [ +# # [], [], [], [], [], [] +# # ] + +# # date = pd.to_datetime(date) ## Ensure date is in datetime format - # ## First get position info - # position_dict, positon_meta = self.parse_position_id(positionID) +# # ## First get position info +# # position_dict, positon_meta = self.parse_position_id(positionID) - # ## Now ensure that the spot and dividend data is available - # for p in position_dict.values(): - # for s in p: - # self.generate_data(swap_ticker(s['ticker'])) - # ticker = swap_ticker(s['ticker']) +# # ## Now ensure that the spot and dividend data is available +# # for p in position_dict.values(): +# # for s in p: +# # self.generate_data(swap_ticker(s['ticker'])) +# # ticker = swap_ticker(s['ticker']) - # ## Get the spot, risk free rate, and dividend yield for the date - # s = self.chain_spot_timeseries[ticker] - # s0_close = self.spot_timeseries[ticker] - # r = self.rf_timeseries - # y = self.dividend_timeseries[ticker] +# # ## Get the spot, risk free rate, and dividend yield for the date +# # s = self.chain_spot_timeseries[ticker] +# # s0_close = self.spot_timeseries[ticker] +# # r = self.rf_timeseries +# # y = self.dividend_timeseries[ticker] - # @log_time(time_logger) - # def get_timeseries(ids, s, r, y, s0_close, direction): - # logger.info("Calculate Greeks dates") - # logger.info(f"Start Date: {self.start_date}") - # logger.info(f"End Date: {self.end_date}") - # full_data = pd.DataFrame() - - # ##ids are a list of tuples, where each tuple is (option_id, shift) - # if ids[-1][0] in self.processed_option_data: - # ## Using -1 index because incases of split, the last id is the one that is subscribed to in the cache - # full_data = self.processed_option_data[ids[-1][0]] ## If the data is already available, then we can skip this step - # logger.info(f"Data for {ids[-1]} already available, skipping calculation") +# # @log_time(time_logger) +# # def get_timeseries(ids, s, r, y, s0_close, direction): +# # logger.info("Calculate Greeks dates") +# # logger.info(f"Start Date: {self.start_date}") +# # logger.info(f"End Date: {self.end_date}") +# # full_data = pd.DataFrame() + +# # ##ids are a list of tuples, where each tuple is (option_id, shift) +# # if ids[-1][0] in self.processed_option_data: +# # ## Using -1 index because incases of split, the last id is the one that is subscribed to in the cache +# # full_data = self.processed_option_data[ids[-1][0]] ## If the data is already available, then we can skip this step +# # logger.info(f"Data for {ids[-1]} already available, skipping calculation") - # else: - # logger.info(f"Data for {ids[-1]} not available, calculating greeks. Load time ~2 minutes") - # for id_set in ids: - # id, shift, start_date = id_set - # data_manager = OptionDataManager(opttick = id) - # self.data_managers[id] = data_manager ## Store the data manager for the option tick - # greeks = data_manager.get_timeseries(start = self.start_date, - # end = self.end_date, - # interval = '1d', - # type_ = 'greeks',).post_processed_data ## Multiply by the shift to account for splits - # greeks = greeks[greeks.index >= start_date] ## Filter the data to only include data after the start date - # greeks_cols = [x for x in greeks.columns if 'Midpoint' in x] - # greeks = greeks[greeks_cols] - # greeks[greeks_cols] = greeks[greeks_cols].replace(0, np.nan).fillna(method = 'ffill') ## FFill NaN values and 0 Values - # greeks.columns = [x.split('_')[1].capitalize() for x in greeks.columns] - - # spot = data_manager.get_timeseries(start = self.start_date, - # end = self.end_date, - # interval = '1d', - # type_ = 'spot', - # extra_cols=['bid', 'ask']).post_processed_data * shift ## Using chain spot data to account for splits - # spot = spot[spot.index >= start_date] ## Filter the data to only include data after the start date - # spot = spot[[self.option_price.capitalize()] + ['Closeask', 'Closebid']] - # data = greeks.join(spot) - # full_data = pd.concat([full_data, data], axis = 0) - # full_data = _clean_data(full_data) - # full_data = full_data[~full_data.index.duplicated(keep = 'last')] - # full_data['s'] = s - # full_data['r'] = r - # full_data['y'] = y - # full_data['s0_close'] = s0_close - # self.processed_option_data[ids[-1][0]] = full_data - # if direction == 'L': - # long.append(full_data) - # elif direction == 'S': - # short.append(full_data) - # else: - # raise ValueError(f"Position Type {_set[0]} not recognized") +# # else: +# # logger.info(f"Data for {ids[-1]} not available, calculating greeks. Load time ~2 minutes") +# # for id_set in ids: +# # id, shift, start_date = id_set +# # data_manager = OptionDataManager(opttick = id) +# # self.data_managers[id] = data_manager ## Store the data manager for the option tick +# # greeks = data_manager.get_timeseries(start = self.start_date, +# # end = self.end_date, +# # interval = '1d', +# # type_ = 'greeks',).post_processed_data ## Multiply by the shift to account for splits +# # greeks = greeks[greeks.index >= start_date] ## Filter the data to only include data after the start date +# # greeks_cols = [x for x in greeks.columns if 'Midpoint' in x] +# # greeks = greeks[greeks_cols] +# # greeks[greeks_cols] = greeks[greeks_cols].replace(0, np.nan).fillna(method = 'ffill') ## FFill NaN values and 0 Values +# # greeks.columns = [x.split('_')[1].capitalize() for x in greeks.columns] + +# # spot = data_manager.get_timeseries(start = self.start_date, +# # end = self.end_date, +# # interval = '1d', +# # type_ = 'spot', +# # extra_cols=['bid', 'ask']).post_processed_data * shift ## Using chain spot data to account for splits +# # spot = spot[spot.index >= start_date] ## Filter the data to only include data after the start date +# # spot = spot[[self.option_price.capitalize()] + ['Closeask', 'Closebid']] +# # data = greeks.join(spot) +# # full_data = pd.concat([full_data, data], axis = 0) +# # full_data = _clean_data(full_data) +# # full_data = full_data[~full_data.index.duplicated(keep = 'last')] +# # full_data['s'] = s +# # full_data['r'] = r +# # full_data['y'] = y +# # full_data['s0_close'] = s0_close +# # self.processed_option_data[ids[-1][0]] = full_data +# # if direction == 'L': +# # long.append(full_data) +# # elif direction == 'S': +# # short.append(full_data) +# # else: +# # raise ValueError(f"Position Type {_set[0]} not recognized") - # return full_data +# # return full_data - # ## Check for splits - # split = self.splits.get(ticker, []) - - # ## Calculating IVs & Greeks for the options - # for _set in positon_meta: - # # To-do: Thread thisto speed up the process - # ids = [(_set[1], 1, self.start_date)] - # if len(split) > 0: - # for i in split: - # split_date = i[0] - # if pd.to_datetime(split_date) < pd.to_datetime(date): ## Strike is already adjusted for the split - # continue - # shift = i[1] - # id = _set[1] - # meta = parse_option_tick(id) - # meta['strike'] = meta['strike'] / shift - # ids.append((generate_option_tick_new(*meta.values()), shift, split_date)) - # # data_manager = OptionDataManager(opttick = id) - - # for input, list_ in zip([ids, s, r, y, s0_close, _set[0]], thread_input_list): - # list_.append(input) +# # ## Check for splits +# # split = self.splits.get(ticker, []) + +# # ## Calculating IVs & Greeks for the options +# # for _set in positon_meta: +# # # To-do: Thread thisto speed up the process +# # ids = [(_set[1], 1, self.start_date)] +# # if len(split) > 0: +# # for i in split: +# # split_date = i[0] +# # if pd.to_datetime(split_date) < pd.to_datetime(date): ## Strike is already adjusted for the split +# # continue +# # shift = i[1] +# # id = _set[1] +# # meta = parse_option_tick(id) +# # meta['strike'] = meta['strike'] / shift +# # ids.append((generate_option_tick_new(*meta.values()), shift, split_date)) +# # # data_manager = OptionDataManager(opttick = id) + +# # for input, list_ in zip([ids, s, r, y, s0_close, _set[0]], thread_input_list): +# # list_.append(input) - # runThreads(get_timeseries, thread_input_list) - # # return long +# # runThreads(get_timeseries, thread_input_list) +# # # return long - # position_data = sum(long) - sum(short) - # position_data = position_data[~position_data.index.duplicated(keep = 'first')] - # position_data.columns = [x.capitalize() for x in position_data.columns] - # ## Retain the spot, risk free rate, and dividend yield for the position, after the greeks have been calculated & spread values subtracted - # position_data['s0_close'] = s0_close - # position_data['s'] = s - # position_data['r'] = r - # position_data['y'] = y - # position_data['spread'] = position_data['Closeask'] - position_data['Closebid'] ## Spread is the difference between the ask and bid prices - # position_data['spread_ratio'] = (position_data['spread'] / position_data['Midpoint'] ).abs().replace(np.inf, np.nan).fillna(0) ## Spread ratio is the spread divided by the midpoint price - # position_data = add_skip_columns(position_data, positionID, ['Delta', 'Gamma', 'Vega', 'Theta', 'Midpoint'], window = 20, skip_threshold=3) - # self.position_data[positionID] = position_data - - - def calculate_position_greeks(self, positionID, date): - """ - Calculate the greeks of a position - - date: Evaluation Date for the greeks (PS: This is not the pricing date) - positionID: str: position string. (PS: This function assumes ticker for position is the same) - """ - logger.info(f"Calculate Greeks Dates Start: {self.start_date}, End: {self.end_date}, Position ID: {positionID}, Date: {date}") - if positionID in self.position_data: - ## If the position data is already available, then we can skip this step - logger.info(f"Position Data for {positionID} already available, skipping calculation") - return self.position_data[positionID] - else: - logger.critical(f"Position Data for {positionID} not available, calculating greeks. Load time ~5 minutes") - ## Initialize the Long and Short Lists - long = [] - short = [] - threads = [] - thread_input_list = [ - [], [] - ] - - date = pd.to_datetime(date) ## Ensure date is in datetime format +# # position_data = sum(long) - sum(short) +# # position_data = position_data[~position_data.index.duplicated(keep = 'first')] +# # position_data.columns = [x.capitalize() for x in position_data.columns] +# # ## Retain the spot, risk free rate, and dividend yield for the position, after the greeks have been calculated & spread values subtracted +# # position_data['s0_close'] = s0_close +# # position_data['s'] = s +# # position_data['r'] = r +# # position_data['y'] = y +# # position_data['spread'] = position_data['Closeask'] - position_data['Closebid'] ## Spread is the difference between the ask and bid prices +# # position_data['spread_ratio'] = (position_data['spread'] / position_data['Midpoint'] ).abs().replace(np.inf, np.nan).fillna(0) ## Spread ratio is the spread divided by the midpoint price +# # position_data = add_skip_columns(position_data, positionID, ['Delta', 'Gamma', 'Vega', 'Theta', 'Midpoint'], window = 20, skip_threshold=3) +# # self.position_data[positionID] = position_data + + +# def calculate_position_greeks(self, positionID, date): +# """ +# Calculate the greeks of a position + +# date: Evaluation Date for the greeks (PS: This is not the pricing date) +# positionID: str: position string. (PS: This function assumes ticker for position is the same) +# """ +# logger.info(f"Calculate Greeks Dates Start: {self.start_date}, End: {self.end_date}, Position ID: {positionID}, Date: {date}") +# if positionID in self.position_data: +# ## If the position data is already available, then we can skip this step +# logger.info(f"Position Data for {positionID} already available, skipping calculation") +# return self.position_data[positionID] +# else: +# logger.critical(f"Position Data for {positionID} not available, calculating greeks. Load time ~5 minutes") +# ## Initialize the Long and Short Lists +# long = [] +# short = [] +# threads = [] +# thread_input_list = [ +# [], [] +# ] + +# date = pd.to_datetime(date) ## Ensure date is in datetime format - ## First get position info - position_dict, positon_meta = self.parse_position_id(positionID) - - ## Now ensure that the spot and dividend data is available - for p in position_dict.values(): - for s in p: - self.generate_data(swap_ticker(s['ticker'])) - ticker = swap_ticker(s['ticker']) - if ticker not in self.spot_timeseries: - self.spot_timeseries[ticker] = self.pm.get_underlier_data(ticker).spot( - ts = True, - ts_start = self.start_date, - ts_end = self.end_date, - ) - - if ticker not in self.chain_spot_timeseries: - self.chain_spot_timeseries[ticker] = self.pm.get_underlier_data(ticker).spot( - ts = True, - ts_start = self.start_date, - ts_end = self.end_date, - spot_type = 'chain_price' - ) - - if ticker not in self.dividend_timeseries: - self.dividend_timeseries[ticker] = self.pm.get_underlier_data(ticker).div_yield_history(start = self.start_date) - - - @log_time(time_logger) - def get_timeseries(_id, direction): - logger.info("Calculate Greeks dates") - logger.info(f"Start Date: {self.start_date}") - logger.info(f"End Date: {self.end_date}") +# ## First get position info +# position_dict, positon_meta = self.parse_position_id(positionID) + +# ## Now ensure that the spot and dividend data is available +# for p in position_dict.values(): +# for s in p: +# self.generate_data(swap_ticker(s['ticker'])) +# ticker = swap_ticker(s['ticker']) +# if ticker not in self.spot_timeseries: +# self.spot_timeseries[ticker] = self.pm.get_underlier_data(ticker).spot( +# ts = True, +# ts_start = self.start_date, +# ts_end = self.end_date, +# ) + +# if ticker not in self.chain_spot_timeseries: +# self.chain_spot_timeseries[ticker] = self.pm.get_underlier_data(ticker).spot( +# ts = True, +# ts_start = self.start_date, +# ts_end = self.end_date, +# spot_type = 'chain_price' +# ) + +# if ticker not in self.dividend_timeseries: +# self.dividend_timeseries[ticker] = self.pm.get_underlier_data(ticker).div_yield_history(start = self.start_date) + + +# @log_time(time_logger) +# def get_timeseries(_id, direction): +# logger.info("Calculate Greeks dates") +# logger.info(f"Start Date: {self.start_date}") +# logger.info(f"End Date: {self.end_date}") - logger.info(f"Calculating Greeks for {_id} on {date} in {direction} direction") - data = self.generate_option_data_for_trade(_id, date) ## Generate the option data for the trade - - if direction == 'L': - long.append(data) - elif direction == 'S': - short.append(data) - else: - raise ValueError(f"Position Type {_set[0]} not recognized") +# logger.info(f"Calculating Greeks for {_id} on {date} in {direction} direction") +# data = self.generate_option_data_for_trade(_id, date) ## Generate the option data for the trade + +# if direction == 'L': +# long.append(data) +# elif direction == 'S': +# short.append(data) +# else: +# raise ValueError(f"Position Type {_set[0]} not recognized") - return data +# return data - ## Calculating IVs & Greeks for the options - for _set in positon_meta: - thread_input_list[0].append(_set[1]) ## Append the option id to the thread input list - thread_input_list[1].append(_set[0]) ## Append the direction to the thread input list - runThreads(get_timeseries, thread_input_list, block=True) ## Run the threads to get the timeseries data for the options +# ## Calculating IVs & Greeks for the options +# for _set in positon_meta: +# thread_input_list[0].append(_set[1]) ## Append the option id to the thread input list +# thread_input_list[1].append(_set[0]) ## Append the direction to the thread input list +# runThreads(get_timeseries, thread_input_list, block=True) ## Run the threads to get the timeseries data for the options - position_data = sum(long) - sum(short) - position_data = position_data[~position_data.index.duplicated(keep = 'first')] - position_data.columns = [x.capitalize() for x in position_data.columns] - ## Retain the spot, risk free rate, and dividend yield for the position, after the greeks have been calculated & spread values subtracted - position_data['s0_close'] = self.spot_timeseries[ticker] ## Spot price at the time of the position - position_data['s'] = self.chain_spot_timeseries[ticker] ## Chain spot price at the time of the position - position_data['r'] = self.rf_timeseries ## Risk free rate at the time of the position - position_data['y'] = self.dividend_timeseries[ticker] ## Dividend yield at the time of the position - position_data['spread'] = position_data['Closeask'] - position_data['Closebid'] ## Spread is the difference between the ask and bid prices - - ## PRICE_ON_TO_DO: No need to change - ## Add the additional columns to the position data - position_data['spread_ratio'] = (position_data['spread'] / position_data['Midpoint'] ).abs().replace(np.inf, np.nan).fillna(0) ## Spread ratio is the spread divided by the midpoint price - position_data = add_skip_columns(position_data, positionID, ['Delta', 'Gamma', 'Vega', 'Theta', 'Midpoint'], window = 20, skip_threshold=3) - self.position_data[positionID] = position_data - - return position_data - - - def load_position_data(self, opttick) -> pd.DataFrame: - """ - Load position data for a given option tick. - - This function ONLY retrives the data for the option tick, it does not apply any splits or adjustments. - This function will NOT check for splits or special dividends. It will only retrieve the data for the given option tick. - """ - ## Get Meta - meta = parse_option_tick(opttick) - return load_position_data(opttick, - self.processed_option_data, - self.start_date, - self.end_date, - s=self.chain_spot_timeseries[meta['ticker']], - r=self.rf_timeseries, - y=self.dividend_timeseries[meta['ticker']], - s0_close=self.spot_timeseries[meta['ticker']],) - - - - def enrich_data(self, data, ticker) -> pd.DataFrame: - """ - Enrich the data with additional information. - """ - return enrich_data(data, ticker, self.spot_timeseries, self.chain_spot_timeseries, self.rf_timeseries, self.dividend_timeseries) +# position_data = sum(long) - sum(short) +# position_data = position_data[~position_data.index.duplicated(keep = 'first')] +# position_data.columns = [x.capitalize() for x in position_data.columns] +# ## Retain the spot, risk free rate, and dividend yield for the position, after the greeks have been calculated & spread values subtracted +# position_data['s0_close'] = self.spot_timeseries[ticker] ## Spot price at the time of the position +# position_data['s'] = self.chain_spot_timeseries[ticker] ## Chain spot price at the time of the position +# position_data['r'] = self.rf_timeseries ## Risk free rate at the time of the position +# position_data['y'] = self.dividend_timeseries[ticker] ## Dividend yield at the time of the position +# position_data['spread'] = position_data['Closeask'] - position_data['Closebid'] ## Spread is the difference between the ask and bid prices + +# ## PRICE_ON_TO_DO: No need to change +# ## Add the additional columns to the position data +# position_data['spread_ratio'] = (position_data['spread'] / position_data['Midpoint'] ).abs().replace(np.inf, np.nan).fillna(0) ## Spread ratio is the spread divided by the midpoint price +# position_data = add_skip_columns(position_data, positionID, ['Delta', 'Gamma', 'Vega', 'Theta', 'Midpoint'], window = 20, skip_threshold=3) +# self.position_data[positionID] = position_data + +# return position_data + + +# def load_position_data(self, opttick) -> pd.DataFrame: +# """ +# Load position data for a given option tick. + +# This function ONLY retrives the data for the option tick, it does not apply any splits or adjustments. +# This function will NOT check for splits or special dividends. It will only retrieve the data for the given option tick. +# """ +# ## Get Meta +# meta = parse_option_tick(opttick) +# return load_position_data(opttick, +# self.processed_option_data, +# self.start_date, +# self.end_date, +# s=self.chain_spot_timeseries[meta['ticker']], +# r=self.rf_timeseries, +# y=self.dividend_timeseries[meta['ticker']], +# s0_close=self.spot_timeseries[meta['ticker']],) + + + +# def enrich_data(self, data, ticker) -> pd.DataFrame: +# """ +# Enrich the data with additional information. +# """ +# return enrich_data(data, ticker, self.spot_timeseries, self.chain_spot_timeseries, self.rf_timeseries, self.dividend_timeseries) - def generate_spot_greeks(self, opttick) -> pd.DataFrame: - """ - Generate spot greeks for a given option tick. - """ - ## PRICE_ON_TO_DO: NO NEED TO CHANGE. This is necessary retrievals - return generate_spot_greeks(opttick, self.start_date, self.end_date) +# def generate_spot_greeks(self, opttick) -> pd.DataFrame: +# """ +# Generate spot greeks for a given option tick. +# """ +# ## PRICE_ON_TO_DO: NO NEED TO CHANGE. This is necessary retrievals +# return generate_spot_greeks(opttick, self.start_date, self.end_date) - def append_option_data(self, - option_id: str=None, - position_data: pd.DataFrame=None, - data_pack: dict|CustomCache=None,): - """ - Append option data to the processed_position_data cache. - Parameters: - position_id: str: ID of the position - position_data: pd.DataFrame: DataFrame containing the position data - data_pack: dict|CustomCache: Data pack containing the position data - """ - if option_id: - assert position_data is not None, "position_data must be provided if option_id is given" - self.processed_option_data[option_id] = position_data +# def append_option_data(self, +# option_id: str=None, +# position_data: pd.DataFrame=None, +# data_pack: dict|CustomCache=None,): +# """ +# Append option data to the processed_position_data cache. +# Parameters: +# position_id: str: ID of the position +# position_data: pd.DataFrame: DataFrame containing the position data +# data_pack: dict|CustomCache: Data pack containing the position data +# """ +# if option_id: +# assert position_data is not None, "position_data must be provided if option_id is given" +# self.processed_option_data[option_id] = position_data - elif data_pack: - # assert isinstance(data_pack, (dict, CustomCache)), "data_pack must be a dict or CustomCache" - for k, v in data_pack.items(): - self.processed_option_data[k] = v +# elif data_pack: +# # assert isinstance(data_pack, (dict, CustomCache)), "data_pack must be a dict or CustomCache" +# for k, v in data_pack.items(): +# self.processed_option_data[k] = v - elif isinstance(data_pack, (CustomCache, dict)): - for k, v in data_pack.items(): - self.processed_option_data[k] = v +# elif isinstance(data_pack, (CustomCache, dict)): +# for k, v in data_pack.items(): +# self.processed_option_data[k] = v - else: - raise ValueError("Either option_id or data_pack must be provided to append_position_data") +# else: +# raise ValueError("Either option_id or data_pack must be provided to append_position_data") - def append_position_data(self, - position_id: str=None, - position_data: pd.DataFrame=None, - data_pack: dict|CustomCache=None,): - """ - Append position data to the position_data cache. - Parameters: - position_id: str: ID of the position - position_data: pd.DataFrame: DataFrame containing the position data - data_pack: dict|CustomCache: Data pack containing the position data - """ - if position_id: - assert position_data is not None, "position_data must be provided if position_id is given" - self.position_data[position_id] = position_data +# def append_position_data(self, +# position_id: str=None, +# position_data: pd.DataFrame=None, +# data_pack: dict|CustomCache=None,): +# """ +# Append position data to the position_data cache. +# Parameters: +# position_id: str: ID of the position +# position_data: pd.DataFrame: DataFrame containing the position data +# data_pack: dict|CustomCache: Data pack containing the position data +# """ +# if position_id: +# assert position_data is not None, "position_data must be provided if position_id is given" +# self.position_data[position_id] = position_data - elif data_pack: - assert isinstance(data_pack, (dict, CustomCache)), "data_pack must be a dict or CustomCache" - for k, v in data_pack.items(): - self.position_data[k] = v +# elif data_pack: +# assert isinstance(data_pack, (dict, CustomCache)), "data_pack must be a dict or CustomCache" +# for k, v in data_pack.items(): +# self.position_data[k] = v - else: - raise ValueError("Either option_id or data_pack must be provided to append_position_data") - - - - def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: - """ - Generate option data for a given trade. - This function retrieve the option data to backtest on. Data will not be saved, as it will be applying splits and adjustments. - This function is written with the assumption that there is no cummulative splits. Expectation is only one split per option tick. - Obviously, this might not be the case if the option was alive for ~5 years or more. But most options are not alive for that long. - """ - - meta = parse_option_tick(opttick) - - ## Check if there's any split/special dividend - splits = self.splits.get(meta['ticker'], []) - dividends = self.special_dividends.get(meta['ticker'], {}) - to_adjust_split = [] - - ## To avoid loading multiple data to account for splits everytime, we check if the PM_date range includes the split date - for pack in splits: - if compare_dates.inbetween( - pack[0], - self.pm_start_date, - self.pm_end_date, - ): - pack = list(pack) ## Convert to list to append later - pack.append('SPLIT') - to_adjust_split.append(pack) - - for pack in dividends.items(): - if compare_dates.inbetween( - pack[0], - self.pm_start_date, - self.pm_end_date, - ): - pack = list(pack) - pack.append('DIVIDEND') - to_adjust_split.append(pack) - - ## Sort the splits by date - to_adjust_split.sort(key=lambda x: x[0]) ## Sort by date - logger.info(f"Splits and Dividends to adjust for {opttick}: {to_adjust_split} range: {self.pm_start_date} to {self.pm_end_date}") - logger.info(f"Splits and Dividends to adjust for {opttick}: {to_adjust_split} range: {self.pm_start_date} to {self.pm_end_date}") - - ## If there are no splits, we can just load the data - if not to_adjust_split: - data = self.load_position_data(opttick).copy() ## Copy to avoid modifying the original data - return data[(data.index >= pd.to_datetime(self.pm_start_date) - relativedelta(months = 3))\ - & (data.index<= pd.to_datetime(self.pm_end_date) + relativedelta(months = 3))] - - # If there are splits, we need to load the data for each tick after adjusting strikes - else: - adj_meta = meta.copy() - adj_strike = meta['strike'] - logger.info(f"Generating data for {opttick} with splits: {to_adjust_split}") - ## Load the data for picked option first - first_set_data = self.load_position_data(opttick).copy() ## Copy to avoid modifying the original data - if compare_dates.is_before(check_date, to_adjust_split[0][0]): - first_set_data = first_set_data[first_set_data.index < to_adjust_split[0][0]] - else: - first_set_data = first_set_data[first_set_data.index >= to_adjust_split[0][0]] - - segments = [] - - for event_date, factor, event_type in to_adjust_split: - if compare_dates.is_before(check_date, event_date): - # You're in the PRE-event regime - if event_type == 'SPLIT': - adj_strike /= factor - elif event_type == 'DIVIDEND': - adj_strike -= factor - else: - # You're in the POST-event regime - if event_type == 'SPLIT': - adj_strike *= factor - elif event_type == 'DIVIDEND': - adj_strike += factor - - adj_opttick = generate_option_tick_new( - symbol=adj_meta['ticker'], - strike=adj_strike, - right=adj_meta['put_call'], - exp=adj_meta['exp_date'] - ) - logger.info(f"Adjusted option tick: {adj_opttick} for event {event_type} on {event_date} with factor {factor}") - - # Load adjusted data - if adj_opttick not in self.processed_option_data: - adj_data = self.load_position_data(adj_opttick).copy() - else: - adj_data = self.processed_option_data[adj_opttick] - - # Slice around the event - if compare_dates.is_before(check_date, event_date): - adj_data = adj_data[adj_data.index >= event_date] - else: - adj_data = adj_data[adj_data.index < event_date] - - # Apply price transformation if SPLIT - ## PRICE_ON_TO_DO: No need to change this. These are necessary columns - if event_type == 'SPLIT': - cols = ['Midpoint', 'Closeask', 'Closebid'] - if compare_dates.is_before(check_date, event_date): - adj_data[cols] *= factor - else: - adj_data[cols] /= factor +# else: +# raise ValueError("Either option_id or data_pack must be provided to append_position_data") + + + +# def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: +# """ +# Generate option data for a given trade. +# This function retrieve the option data to backtest on. Data will not be saved, as it will be applying splits and adjustments. +# This function is written with the assumption that there is no cummulative splits. Expectation is only one split per option tick. +# Obviously, this might not be the case if the option was alive for ~5 years or more. But most options are not alive for that long. +# """ + +# meta = parse_option_tick(opttick) + +# ## Check if there's any split/special dividend +# splits = self.splits.get(meta['ticker'], []) +# dividends = self.special_dividends.get(meta['ticker'], {}) +# to_adjust_split = [] + +# ## To avoid loading multiple data to account for splits everytime, we check if the PM_date range includes the split date +# for pack in splits: +# if compare_dates.inbetween( +# pack[0], +# self.pm_start_date, +# self.pm_end_date, +# ): +# pack = list(pack) ## Convert to list to append later +# pack.append('SPLIT') +# to_adjust_split.append(pack) + +# for pack in dividends.items(): +# if compare_dates.inbetween( +# pack[0], +# self.pm_start_date, +# self.pm_end_date, +# ): +# pack = list(pack) +# pack.append('DIVIDEND') +# to_adjust_split.append(pack) + +# ## Sort the splits by date +# to_adjust_split.sort(key=lambda x: x[0]) ## Sort by date +# logger.info(f"Splits and Dividends to adjust for {opttick}: {to_adjust_split} range: {self.pm_start_date} to {self.pm_end_date}") +# logger.info(f"Splits and Dividends to adjust for {opttick}: {to_adjust_split} range: {self.pm_start_date} to {self.pm_end_date}") + +# ## If there are no splits, we can just load the data +# if not to_adjust_split: +# data = self.load_position_data(opttick).copy() ## Copy to avoid modifying the original data +# return data[(data.index >= pd.to_datetime(self.pm_start_date) - relativedelta(months = 3))\ +# & (data.index<= pd.to_datetime(self.pm_end_date) + relativedelta(months = 3))] + +# # If there are splits, we need to load the data for each tick after adjusting strikes +# else: +# adj_meta = meta.copy() +# adj_strike = meta['strike'] +# logger.info(f"Generating data for {opttick} with splits: {to_adjust_split}") +# ## Load the data for picked option first +# first_set_data = self.load_position_data(opttick).copy() ## Copy to avoid modifying the original data +# if compare_dates.is_before(check_date, to_adjust_split[0][0]): +# first_set_data = first_set_data[first_set_data.index < to_adjust_split[0][0]] +# else: +# first_set_data = first_set_data[first_set_data.index >= to_adjust_split[0][0]] + +# segments = [] + +# for event_date, factor, event_type in to_adjust_split: +# if compare_dates.is_before(check_date, event_date): +# # You're in the PRE-event regime +# if event_type == 'SPLIT': +# adj_strike /= factor +# elif event_type == 'DIVIDEND': +# adj_strike -= factor +# else: +# # You're in the POST-event regime +# if event_type == 'SPLIT': +# adj_strike *= factor +# elif event_type == 'DIVIDEND': +# adj_strike += factor + +# adj_opttick = generate_option_tick_new( +# symbol=adj_meta['ticker'], +# strike=adj_strike, +# right=adj_meta['put_call'], +# exp=adj_meta['exp_date'] +# ) +# logger.info(f"Adjusted option tick: {adj_opttick} for event {event_type} on {event_date} with factor {factor}") + +# # Load adjusted data +# if adj_opttick not in self.processed_option_data: +# adj_data = self.load_position_data(adj_opttick).copy() +# else: +# adj_data = self.processed_option_data[adj_opttick] + +# # Slice around the event +# if compare_dates.is_before(check_date, event_date): +# adj_data = adj_data[adj_data.index >= event_date] +# else: +# adj_data = adj_data[adj_data.index < event_date] + +# # Apply price transformation if SPLIT +# ## PRICE_ON_TO_DO: No need to change this. These are necessary columns +# if event_type == 'SPLIT': +# cols = ['Midpoint', 'Closeask', 'Closebid'] +# if compare_dates.is_before(check_date, event_date): +# adj_data[cols] *= factor +# else: +# adj_data[cols] /= factor - segments.append(adj_data) +# segments.append(adj_data) - base_data = self.load_position_data(opttick).copy() - first_event_date = to_adjust_split[0][0] if to_adjust_split else self.pm_start_date - if compare_dates.is_before(check_date, first_event_date): - base_data = base_data[base_data.index < first_event_date] +# base_data = self.load_position_data(opttick).copy() +# first_event_date = to_adjust_split[0][0] if to_adjust_split else self.pm_start_date +# if compare_dates.is_before(check_date, first_event_date): +# base_data = base_data[base_data.index < first_event_date] - else: - base_data = base_data[base_data.index >= first_event_date] +# else: +# base_data = base_data[base_data.index >= first_event_date] - segments.insert(0, base_data) - final_data = pd.concat(segments).sort_index() - final_data = final_data[~final_data.index.duplicated(keep='last')] +# segments.insert(0, base_data) +# final_data = pd.concat(segments).sort_index() +# final_data = final_data[~final_data.index.duplicated(keep='last')] - ## Leave residual data outside the PM date range - final_data = final_data[(final_data.index >= pd.to_datetime(self.pm_start_date) - relativedelta(months = 3)) & \ - (final_data.index <= pd.to_datetime(self.pm_end_date) + relativedelta(months = 3))] - return final_data - - @log_time(time_logger) - def update_greek_limits(self, signal_id, position_id) -> None: - """ - Updates the limits associated with a signal - ps: This should only be updated on first purchase of the signal - Limits are saved in absolute values to account for both long and short positions +# ## Leave residual data outside the PM date range +# final_data = final_data[(final_data.index >= pd.to_datetime(self.pm_start_date) - relativedelta(months = 3)) & \ +# (final_data.index <= pd.to_datetime(self.pm_end_date) + relativedelta(months = 3))] +# return final_data + +# @log_time(time_logger) +# def update_greek_limits(self, signal_id, position_id) -> None: +# """ +# Updates the limits associated with a signal +# ps: This should only be updated on first purchase of the signal +# Limits are saved in absolute values to account for both long and short positions - """ +# """ - if signal_id in self.greek_limits['delta'] and not self.re_update_on_roll: ## May consider to maximize cash on roll - logger.info(f"Greek Limits for Signal ID: {signal_id} already updated, skipping") - return - logger.info(f"Updating Greek Limits for Signal ID: {signal_id} and Position ID: {position_id}") - id_details = parse_signal_id(signal_id) - cash_available = self.pm.allocated_cash_map[swap_ticker(id_details['ticker'])] - delta_at_purchase = self.position_data[position_id]['Delta'][id_details['date']] - s0_at_purchase = self.position_data[position_id]['s'][id_details['date']] ## As always, we use the chain spot data to account for splits - equivalent_delta_size = ((cash_available/s0_at_purchase)/100) * self.sizing_lev - self.greek_limits['delta'][signal_id] = abs(equivalent_delta_size) - logger.info(f"Spot Price at Purchase: {s0_at_purchase} at time {id_details['date']}") - logger.info(f"Delta at Purchase: {delta_at_purchase}") - logger.info(f"Equivalent Delta Size: {equivalent_delta_size}, with Cash Available: {cash_available}, and Leverage: {self.sizing_lev}") - logger.info(f"Equivalent Delta Size: {equivalent_delta_size}") - - def calculate_quantity(self, positionID, signalID, date, opt_price) -> int: - """ - Returns the quantity of the position that can be bought based on the sizing type - """ - logger.info(f"Calculating Quantity for Position ID: {positionID} and Signal ID: {signalID} on Date: {date}") - if positionID not in self.position_data: ## If the position data isn't available, calculate the greeks - self.calculate_position_greeks(positionID, date) +# if signal_id in self.greek_limits['delta'] and not self.re_update_on_roll: ## May consider to maximize cash on roll +# logger.info(f"Greek Limits for Signal ID: {signal_id} already updated, skipping") +# return +# logger.info(f"Updating Greek Limits for Signal ID: {signal_id} and Position ID: {position_id}") +# id_details = parse_signal_id(signal_id) +# cash_available = self.pm.allocated_cash_map[swap_ticker(id_details['ticker'])] +# delta_at_purchase = self.position_data[position_id]['Delta'][id_details['date']] +# s0_at_purchase = self.position_data[position_id]['s'][id_details['date']] ## As always, we use the chain spot data to account for splits +# equivalent_delta_size = ((cash_available/s0_at_purchase)/100) * self.sizing_lev +# self.greek_limits['delta'][signal_id] = abs(equivalent_delta_size) +# logger.info(f"Spot Price at Purchase: {s0_at_purchase} at time {id_details['date']}") +# logger.info(f"Delta at Purchase: {delta_at_purchase}") +# logger.info(f"Equivalent Delta Size: {equivalent_delta_size}, with Cash Available: {cash_available}, and Leverage: {self.sizing_lev}") +# logger.info(f"Equivalent Delta Size: {equivalent_delta_size}") + +# def calculate_quantity(self, positionID, signalID, date, opt_price) -> int: +# """ +# Returns the quantity of the position that can be bought based on the sizing type +# """ +# logger.info(f"Calculating Quantity for Position ID: {positionID} and Signal ID: {signalID} on Date: {date}") +# if positionID not in self.position_data: ## If the position data isn't available, calculate the greeks +# self.calculate_position_greeks(positionID, date) - ## First get position info and ticker - position_dict, _ = self.parse_position_id(positionID) - key = list(position_dict.keys())[0] - ticker = swap_ticker(position_dict[key][0]['ticker']) - - ## Now calculate the max size cash can buy - cash_available = self.pm.allocated_cash_map[ticker] - purchase_date = pd.to_datetime(date) - s0_at_purchase = self.position_data[positionID]['s'][purchase_date] ## s -> chain spot, s0_close -> adjusted close - logger.info(f"Spot Price at Purchase: {s0_at_purchase} at time {purchase_date}") - logger.info(f"Cash Available: {cash_available}, Option Price: {opt_price}, Cash_Available/OptPRice: {(cash_available/(opt_price*100))}") - max_size_cash_can_buy = abs(math.floor(cash_available/(opt_price*100))) ## Assuming Allocated Cash map is already in 100s - - if self.sizing_type == 'price': - return max_size_cash_can_buy +# ## First get position info and ticker +# position_dict, _ = self.parse_position_id(positionID) +# key = list(position_dict.keys())[0] +# ticker = swap_ticker(position_dict[key][0]['ticker']) + +# ## Now calculate the max size cash can buy +# cash_available = self.pm.allocated_cash_map[ticker] +# purchase_date = pd.to_datetime(date) +# s0_at_purchase = self.position_data[positionID]['s'][purchase_date] ## s -> chain spot, s0_close -> adjusted close +# logger.info(f"Spot Price at Purchase: {s0_at_purchase} at time {purchase_date}") +# logger.info(f"Cash Available: {cash_available}, Option Price: {opt_price}, Cash_Available/OptPRice: {(cash_available/(opt_price*100))}") +# max_size_cash_can_buy = abs(math.floor(cash_available/(opt_price*100))) ## Assuming Allocated Cash map is already in 100s + +# if self.sizing_type == 'price': +# return max_size_cash_can_buy - elif self.sizing_type.capitalize() == 'Delta': - delta = self.position_data[positionID]['Delta'][purchase_date] - if signalID not in self.greek_limits['delta']: - self.update_greek_limits(signalID,positionID ) - target_delta = self.greek_limits['delta'][signalID] - logger.info(f"Target Delta: {target_delta}") - delta_size = (math.floor(target_delta/abs(delta))) - logger.info(f"Delta from Full Cash Spend: {max_size_cash_can_buy * delta}, Size: {max_size_cash_can_buy}") - logger.info(f"Delta with Size Limit: {delta_size * delta}, Size: {delta_size}") - return delta_size if abs(delta_size) <= abs(max_size_cash_can_buy) else max_size_cash_can_buy +# elif self.sizing_type.capitalize() == 'Delta': +# delta = self.position_data[positionID]['Delta'][purchase_date] +# if signalID not in self.greek_limits['delta']: +# self.update_greek_limits(signalID,positionID ) +# target_delta = self.greek_limits['delta'][signalID] +# logger.info(f"Target Delta: {target_delta}") +# delta_size = (math.floor(target_delta/abs(delta))) +# logger.info(f"Delta from Full Cash Spend: {max_size_cash_can_buy * delta}, Size: {max_size_cash_can_buy}") +# logger.info(f"Delta with Size Limit: {delta_size * delta}, Size: {delta_size}") +# return delta_size if abs(delta_size) <= abs(max_size_cash_can_buy) else max_size_cash_can_buy - elif self.sizing_type.capitalize() in ['Gamma', 'Vega']: - raise NotImplementedError(f"Sizing Type {self.sizing_type} not yet implemented, please use 'delta' or 'price'") +# elif self.sizing_type.capitalize() in ['Gamma', 'Vega']: +# raise NotImplementedError(f"Sizing Type {self.sizing_type} not yet implemented, please use 'delta' or 'price'") - else: - raise ValueError(f"Sizing Type {self.sizing_type} not recognized") +# else: +# raise ValueError(f"Sizing Type {self.sizing_type} not recognized") - def analyze_position(self): - """ - Analyze the current positions and determine if any need to be rolled, closed, or adjusted - """ - position_action_dict = {} ## This will be used to store the actions for each position - date = pd.to_datetime(self.pm.eventScheduler.current_date) - if date in self.__analyzed_date_list: ## If the date has already been analyzed, return - logger.info(f"Positions already analyzed on {date}, skipping") - return "ALREADY_ANALYZED" +# def analyze_position(self): +# """ +# Analyze the current positions and determine if any need to be rolled, closed, or adjusted +# """ +# position_action_dict = {} ## This will be used to store the actions for each position +# date = pd.to_datetime(self.pm.eventScheduler.current_date) +# if date in self.__analyzed_date_list: ## If the date has already been analyzed, return +# logger.info(f"Positions already analyzed on {date}, skipping") +# return "ALREADY_ANALYZED" - self.__analyzed_date_list.append(date) ## Add the date to the analyzed list - event_date = pd.to_datetime(date) + BDay(self.t_plus_n) ## Order date is the next business day after the current date - logger.info(f"Analyzing Positions on {date}") - is_holiday = is_USholiday(date) - if is_holiday: - self.pm.logger.warning(f"Market is closed on {date}, skipping") - logger.info(f"Market is closed on {date}, skipping") - return "IS_HOLIDAY" - - ## First check if the position needs to be rolled - if self.limits['dte']: - roll_dict = self.dte_check() - else: - logger.info("Roll Check Not Enabled") - roll_dict = {} - for sym in self.pm.symbol_list: - current_position = self.pm.current_positions[sym] - if 'position' not in current_position: - continue - roll_dict[current_position['position']['trade_id']] = HOLD(current_position['position']['trade_id']) - - logger.info(f"Roll Dict {roll_dict}") - - ## Check if the position needs to be adjusted based on moneyness - if self.limits['moneyness']: - moneyness_dict = self.moneyness_check() - else: - logger.info("Moneyness Check Not Enabled") - moneyness_dict = {} - for sym in self.pm.symbol_list: - current_position = self.pm.current_positions[sym] - if 'position' not in current_position: - continue - moneyness_dict[current_position['position']['trade_id']] = HOLD(current_position['position']['trade_id']) - logger.info(f"Moneyness Dict: {moneyness_dict}") - - ## Check if the position needs to be adjusted based on greeks - greek_dict = self.limits_check() - logger.info(f"Greek Dict {greek_dict}") - - check_dicts = [roll_dict, moneyness_dict, greek_dict] - all_empty = all([len(x)==0 for x in check_dicts]) - - if all_empty: ## Return if all are empty - self.pm.logger.info(f"No positions need to be adjusted on {date}") - print(f"No positions need to be adjusted on {date}") - return "NO_POSITIONS_TO_ADJUST" +# self.__analyzed_date_list.append(date) ## Add the date to the analyzed list +# event_date = pd.to_datetime(date) + BDay(self.t_plus_n) ## Order date is the next business day after the current date +# logger.info(f"Analyzing Positions on {date}") +# is_holiday = is_USholiday(date) +# if is_holiday: +# self.pm.logger.warning(f"Market is closed on {date}, skipping") +# logger.info(f"Market is closed on {date}, skipping") +# return "IS_HOLIDAY" + +# ## First check if the position needs to be rolled +# if self.limits['dte']: +# roll_dict = self.dte_check() +# else: +# logger.info("Roll Check Not Enabled") +# roll_dict = {} +# for sym in self.pm.symbol_list: +# current_position = self.pm.current_positions[sym] +# if 'position' not in current_position: +# continue +# roll_dict[current_position['position']['trade_id']] = HOLD(current_position['position']['trade_id']) + +# logger.info(f"Roll Dict {roll_dict}") + +# ## Check if the position needs to be adjusted based on moneyness +# if self.limits['moneyness']: +# moneyness_dict = self.moneyness_check() +# else: +# logger.info("Moneyness Check Not Enabled") +# moneyness_dict = {} +# for sym in self.pm.symbol_list: +# current_position = self.pm.current_positions[sym] +# if 'position' not in current_position: +# continue +# moneyness_dict[current_position['position']['trade_id']] = HOLD(current_position['position']['trade_id']) +# logger.info(f"Moneyness Dict: {moneyness_dict}") + +# ## Check if the position needs to be adjusted based on greeks +# greek_dict = self.limits_check() +# logger.info(f"Greek Dict {greek_dict}") + +# check_dicts = [roll_dict, moneyness_dict, greek_dict] +# all_empty = all([len(x)==0 for x in check_dicts]) + +# if all_empty: ## Return if all are empty +# self.pm.logger.info(f"No positions need to be adjusted on {date}") +# print(f"No positions need to be adjusted on {date}") +# return "NO_POSITIONS_TO_ADJUST" - actions_dicts = { - 'dte': roll_dict, - 'moneyness': moneyness_dict, - 'greeks': greek_dict - } - ## Aggregate the results - trades_df = self.unadjusted_signals - bars_trade = self.bars.trades_df - for sym in self.pm.symbol_list: - position = self.pm.current_positions[sym] - for signal_id, current_position in position.items(): - if 'position' not in current_position: - continue - k = current_position['position']['trade_id'] - exit_signal_date = trades_df[trades_df['signal_id'] == signal_id].ExitTime.values[0] ## This is not look ahead because Signal is gotten on bars_df - t_plus_n - entry_signal_date = trades_df[trades_df['signal_id'] == signal_id].EntryTime.values[0] ## This is not look ahead because Signal is gotten on bars_df - t_plus_n - exit_date, entry_date = bars_trade[bars_trade['signal_id'] == signal_id].ExitTime.values[0], bars_trade[bars_trade['signal_id'] == signal_id].EntryTime.values[0] - if compare_dates.is_on_or_after(date, exit_signal_date) or compare_dates.is_on_or_before(date, entry_date): - logger.info(f"Position has exited on {exit_signal_date} or not yet entered on {entry_date}, skipping") - continue +# actions_dicts = { +# 'dte': roll_dict, +# 'moneyness': moneyness_dict, +# 'greeks': greek_dict +# } +# ## Aggregate the results +# trades_df = self.unadjusted_signals +# bars_trade = self.bars.trades_df +# for sym in self.pm.symbol_list: +# position = self.pm.current_positions[sym] +# for signal_id, current_position in position.items(): +# if 'position' not in current_position: +# continue +# k = current_position['position']['trade_id'] +# exit_signal_date = trades_df[trades_df['signal_id'] == signal_id].ExitTime.values[0] ## This is not look ahead because Signal is gotten on bars_df - t_plus_n +# entry_signal_date = trades_df[trades_df['signal_id'] == signal_id].EntryTime.values[0] ## This is not look ahead because Signal is gotten on bars_df - t_plus_n +# exit_date, entry_date = bars_trade[bars_trade['signal_id'] == signal_id].ExitTime.values[0], bars_trade[bars_trade['signal_id'] == signal_id].EntryTime.values[0] +# if compare_dates.is_on_or_after(date, exit_signal_date) or compare_dates.is_on_or_before(date, entry_date): +# logger.info(f"Position has exited on {exit_signal_date} or not yet entered on {entry_date}, skipping") +# continue - ## There are 4 possible actions: roll, Hold, Exercise, Adjust - ## Roll happens on DTE & Moneyness. Exercise happens on DTE. Adjust happens on Greeks - actions = [] - reasons = [] - for action in actions_dicts: - if k in actions_dicts[action]: - actions.append(actions_dicts[action][k]) - reasons.append(action) - else: - actions.append(EventTypes.HOLD.value) - reasons.append('hold') +# ## There are 4 possible actions: roll, Hold, Exercise, Adjust +# ## Roll happens on DTE & Moneyness. Exercise happens on DTE. Adjust happens on Greeks +# actions = [] +# reasons = [] +# for action in actions_dicts: +# if k in actions_dicts[action]: +# actions.append(actions_dicts[action][k]) +# reasons.append(action) +# else: +# actions.append(EventTypes.HOLD.value) +# reasons.append('hold') - sub_action_dict = {'action': '', 'quantity_diff': 0} +# sub_action_dict = {'action': '', 'quantity_diff': 0} - ## If the position needs to be rolled or exercised, do that first, no need to check other actions or adjust quantity - if EventTypes.ROLL.value in actions: - pos_action = ROLL(k, {}) - pos_action.reason = reasons[actions.index(EventTypes.ROLL.value)] +# ## If the position needs to be rolled or exercised, do that first, no need to check other actions or adjust quantity +# if EventTypes.ROLL.value in actions: +# pos_action = ROLL(k, {}) +# pos_action.reason = reasons[actions.index(EventTypes.ROLL.value)] - event = RollEvent( - datetime = event_date, - symbol = sym, - signal_type = parse_signal_id(signal_id)['direction'], - position = current_position, - signal_id = signal_id - - ) - pos_action.event = event - position_action_dict[k] = pos_action - continue - - ## If exercise is needed, do that first, no need to check other actions or adjust quantity - elif EventTypes.EXERCISE.value in actions: - pos_action = EXERCISE(k, {}) - pos_action.reason = reasons[actions.index(EventTypes.EXERCISE.value)] - long_premiums, short_premiums = self.pm.get_premiums_on_position(current_position['position'], date) +# event = RollEvent( +# datetime = event_date, +# symbol = sym, +# signal_type = parse_signal_id(signal_id)['direction'], +# position = current_position, +# signal_id = signal_id + +# ) +# pos_action.event = event +# position_action_dict[k] = pos_action +# continue + +# ## If exercise is needed, do that first, no need to check other actions or adjust quantity +# elif EventTypes.EXERCISE.value in actions: +# pos_action = EXERCISE(k, {}) +# pos_action.reason = reasons[actions.index(EventTypes.EXERCISE.value)] +# long_premiums, short_premiums = self.pm.get_premiums_on_position(current_position['position'], date) - event = ExerciseEvent( - datetime = date, ## Exercise happens on the same day as the action. - symbol = sym, - quantity = current_position['quantity'], - entry_date = date, - spot = self.chain_spot_timeseries[sym][date], ## Using chain spot because strikes are unadjusted for splits - long_premiums = long_premiums, - short_premiums = short_premiums, - position = current_position, - signal_id = signal_id +# event = ExerciseEvent( +# datetime = date, ## Exercise happens on the same day as the action. +# symbol = sym, +# quantity = current_position['quantity'], +# entry_date = date, +# spot = self.chain_spot_timeseries[sym][date], ## Using chain spot because strikes are unadjusted for splits +# long_premiums = long_premiums, +# short_premiums = short_premiums, +# position = current_position, +# signal_id = signal_id - ) - pos_action.event = event - sub_action_dict[k] = pos_action +# ) +# pos_action.event = event +# sub_action_dict[k] = pos_action - continue +# continue - ## If the position is a hold, check if it needs to be adjusted based on greeks - elif EventTypes.HOLD.value in actions: - pos_action = HOLD(k) - pos_action.reason = None - position_action_dict[k] = pos_action - - quantity_change_list = [0] ## Initialize the quantity change list with 0 - value = greek_dict.get(k, {}) ## Get the greek dict for each position - for greek, res in value.items(): ## Looping through each greek adjustments - quantity_change_list.append(res['quantity_diff']) - sub_action_dict['quantity_diff'] = min(quantity_change_list) ## Ultimate adjustment would be the minimum reduction factor because they're all negative values - if sub_action_dict['quantity_diff'] < 0: ## If the quantity needs to be reduced, set the action to adjust - pos_action = ADJUST(k, sub_action_dict) - pos_action.reason = "greek_limit" - - event = OrderEvent( - symbol = sym, - datetime = event_date, - order_type = 'MKT', - quantity= abs(sub_action_dict['quantity_diff']), - direction = 'SELL' if sub_action_dict['quantity_diff'] < 0 else 'BUY', - position = current_position['position'], - signal_id = signal_id - ) - pos_action.event = event - position_action_dict[k] = pos_action ## If adjust position, override HOLD. - self._actions[date] = position_action_dict - logger.info(f"Position Action Dict: {position_action_dict}") - for id, action in position_action_dict.items(): - logger.info(f"Position ID: {id}, Action: {action}, Reason: {action.reason}") - if not isinstance(action, HOLD): - logger.info(f"Event: {action.event}") - logger.info((f"Risk Manager Scheduling Action: Position ID: {id}, Action: {action}, Reason: {action.reason}")) - self.pm.eventScheduler.schedule_event(event_date, action.event) - - return position_action_dict +# ## If the position is a hold, check if it needs to be adjusted based on greeks +# elif EventTypes.HOLD.value in actions: +# pos_action = HOLD(k) +# pos_action.reason = None +# position_action_dict[k] = pos_action + +# quantity_change_list = [0] ## Initialize the quantity change list with 0 +# value = greek_dict.get(k, {}) ## Get the greek dict for each position +# for greek, res in value.items(): ## Looping through each greek adjustments +# quantity_change_list.append(res['quantity_diff']) +# sub_action_dict['quantity_diff'] = min(quantity_change_list) ## Ultimate adjustment would be the minimum reduction factor because they're all negative values +# if sub_action_dict['quantity_diff'] < 0: ## If the quantity needs to be reduced, set the action to adjust +# pos_action = ADJUST(k, sub_action_dict) +# pos_action.reason = "greek_limit" + +# event = OrderEvent( +# symbol = sym, +# datetime = event_date, +# order_type = 'MKT', +# quantity= abs(sub_action_dict['quantity_diff']), +# direction = 'SELL' if sub_action_dict['quantity_diff'] < 0 else 'BUY', +# position = current_position['position'], +# signal_id = signal_id +# ) +# pos_action.event = event +# position_action_dict[k] = pos_action ## If adjust position, override HOLD. +# self._actions[date] = position_action_dict +# logger.info(f"Position Action Dict: {position_action_dict}") +# for id, action in position_action_dict.items(): +# logger.info(f"Position ID: {id}, Action: {action}, Reason: {action.reason}") +# if not isinstance(action, HOLD): +# logger.info(f"Event: {action.event}") +# logger.info((f"Risk Manager Scheduling Action: Position ID: {id}, Action: {action}, Reason: {action.reason}")) +# self.pm.eventScheduler.schedule_event(event_date, action.event) + +# return position_action_dict - def limits_check(self): - limits = self.limits - delta_limit = limits['delta'] - position_limit = {} - - date = pd.to_datetime(self.pm.eventScheduler.current_date) - if is_USholiday(date): - self.pm.logger.warning(f"Market is closed on {date}, skipping") - return - - for symbol in self.pm.symbol_list: - position = self.pm.current_positions[symbol] - for signal_id, current_position in position.items(): - if 'position' not in current_position: - continue - logger.info(f"Checking Position {current_position['position']['trade_id']} for Greek Limits on {date}") - trade_id = current_position['position']['trade_id'] - quantity = current_position['quantity'] - signal_id = signal_id - max_delta = self.greek_limits['delta'][signal_id] - pos_data = self.position_data[trade_id] - - status = {'status': False, 'quantity_diff': 0} - greek_limit_bool = dict(delta=status, gamma=status, vega=status, theta=status) - - if delta_limit: - delta_val = abs(pos_data.at[date, 'Delta']) - skip = pos_data.at[date, 'Delta_skip_day'] if 'Delta_skip_day' in pos_data.columns else False - - if skip or delta_val == 0: - continue - - current_delta = delta_val * quantity - if current_delta > max_delta: - # Compute how many contracts to reduce - required_quantity = max(int(max_delta // delta_val), 1) ## Ensure at least 1 contract is required. If last contract exceeds delta limit, we will still hold it. - quantity_diff = required_quantity - quantity - logger.info(f"Position {trade_id} exceeds delta limit. Current Delta: {current_delta}, Max Delta: {max_delta}, Required Quantity: {required_quantity}, Current Quantity: {quantity}") - greek_limit_bool['delta'] = {'status': True, 'quantity_diff': quantity_diff} - - position_limit[trade_id] = greek_limit_bool - - return position_limit - - - - - def dte_check(self): - """ - Analyze the current positions and determine if any need to be rolled - """ - date = pd.to_datetime(self.pm.eventScheduler.current_date) - logger.info(f"Checking DTE on {date}") - if is_USholiday(date): - self.pm.logger.warning(f"Market is closed on {date}, skipping") - return +# def limits_check(self): +# limits = self.limits +# delta_limit = limits['delta'] +# position_limit = {} + +# date = pd.to_datetime(self.pm.eventScheduler.current_date) +# if is_USholiday(date): +# self.pm.logger.warning(f"Market is closed on {date}, skipping") +# return + +# for symbol in self.pm.symbol_list: +# position = self.pm.current_positions[symbol] +# for signal_id, current_position in position.items(): +# if 'position' not in current_position: +# continue +# logger.info(f"Checking Position {current_position['position']['trade_id']} for Greek Limits on {date}") +# trade_id = current_position['position']['trade_id'] +# quantity = current_position['quantity'] +# signal_id = signal_id +# max_delta = self.greek_limits['delta'][signal_id] +# pos_data = self.position_data[trade_id] + +# status = {'status': False, 'quantity_diff': 0} +# greek_limit_bool = dict(delta=status, gamma=status, vega=status, theta=status) + +# if delta_limit: +# delta_val = abs(pos_data.at[date, 'Delta']) +# skip = pos_data.at[date, 'Delta_skip_day'] if 'Delta_skip_day' in pos_data.columns else False + +# if skip or delta_val == 0: +# continue + +# current_delta = delta_val * quantity +# if current_delta > max_delta: +# # Compute how many contracts to reduce +# required_quantity = max(int(max_delta // delta_val), 1) ## Ensure at least 1 contract is required. If last contract exceeds delta limit, we will still hold it. +# quantity_diff = required_quantity - quantity +# logger.info(f"Position {trade_id} exceeds delta limit. Current Delta: {current_delta}, Max Delta: {max_delta}, Required Quantity: {required_quantity}, Current Quantity: {quantity}") +# greek_limit_bool['delta'] = {'status': True, 'quantity_diff': quantity_diff} + +# position_limit[trade_id] = greek_limit_bool + +# return position_limit + + + + +# def dte_check(self): +# """ +# Analyze the current positions and determine if any need to be rolled +# """ +# date = pd.to_datetime(self.pm.eventScheduler.current_date) +# logger.info(f"Checking DTE on {date}") +# if is_USholiday(date): +# self.pm.logger.warning(f"Market is closed on {date}, skipping") +# return - roll_dict = {} - for symbol in self.pm.symbol_list: - position = self.pm.current_positions[symbol] - for signal_id, current_position in position.items(): - if 'position' not in current_position: - continue +# roll_dict = {} +# for symbol in self.pm.symbol_list: +# position = self.pm.current_positions[symbol] +# for signal_id, current_position in position.items(): +# if 'position' not in current_position: +# continue - logger.info(f"Checking Position {current_position['position']['trade_id']} for DTE on {date}") - id = current_position['position']['trade_id'] - expiry_date = '' +# logger.info(f"Checking Position {current_position['position']['trade_id']} for DTE on {date}") +# id = current_position['position']['trade_id'] +# expiry_date = '' - if 'long' in current_position['position']: - for option_id in current_position['position']['long']: - option_meta = parse_option_tick(option_id) - expiry_date = option_meta['exp_date'] - break - elif 'short' in current_position['position']: - for option_id in current_position['position']['short']: - option_meta = parse_option_tick(option_id) - expiry_date = option_meta['exp_date'] - break - - - dte = (pd.to_datetime(expiry_date) - pd.to_datetime(date)).days - logger.info(f"ID: {id}, DTE: {dte}, Expiry: {expiry_date}, Date: {date}") - - if symbol in self.pm.roll_map and dte <= self.pm.roll_map[symbol]: - logger.info(f"{id} rolling because {dte} <= {self.pm.roll_map[symbol]}") - roll_dict[id] = EventTypes.ROLL.value - elif symbol not in self.pm.roll_map and dte == 0: # exercise contract if symbol not in roll map - logger.info(f"{id} exercising because {dte} == 0") - roll_dict[id] = EventTypes.EXERCISE.value - else: - logger.info(f"{id} holding because {dte} > {self.pm.roll_map[symbol]}") - roll_dict[id] = EventTypes.HOLD.value - return roll_dict +# if 'long' in current_position['position']: +# for option_id in current_position['position']['long']: +# option_meta = parse_option_tick(option_id) +# expiry_date = option_meta['exp_date'] +# break +# elif 'short' in current_position['position']: +# for option_id in current_position['position']['short']: +# option_meta = parse_option_tick(option_id) +# expiry_date = option_meta['exp_date'] +# break + + +# dte = (pd.to_datetime(expiry_date) - pd.to_datetime(date)).days +# logger.info(f"ID: {id}, DTE: {dte}, Expiry: {expiry_date}, Date: {date}") + +# if symbol in self.pm.roll_map and dte <= self.pm.roll_map[symbol]: +# logger.info(f"{id} rolling because {dte} <= {self.pm.roll_map[symbol]}") +# roll_dict[id] = EventTypes.ROLL.value +# elif symbol not in self.pm.roll_map and dte == 0: # exercise contract if symbol not in roll map +# logger.info(f"{id} exercising because {dte} == 0") +# roll_dict[id] = EventTypes.EXERCISE.value +# else: +# logger.info(f"{id} holding because {dte} > {self.pm.roll_map[symbol]}") +# roll_dict[id] = EventTypes.HOLD.value +# return roll_dict - def moneyness_check(self): - """ - Analyze the current positions and determine if any need to be rolled based on moneyness - """ - date = pd.to_datetime(self.pm.eventScheduler.current_date) - logger.info(f"Checking Moneyness on {date}") - if is_USholiday(date): - self.pm.logger.warning(f"Market is closed on {date}, skipping") - return +# def moneyness_check(self): +# """ +# Analyze the current positions and determine if any need to be rolled based on moneyness +# """ +# date = pd.to_datetime(self.pm.eventScheduler.current_date) +# logger.info(f"Checking Moneyness on {date}") +# if is_USholiday(date): +# self.pm.logger.warning(f"Market is closed on {date}, skipping") +# return - roll_dict = {} - for symbol in self.pm.symbol_list: - strike_list = [] - position = self.pm.current_positions[symbol] - for signal_id, current_position in position.items(): - if 'position' not in current_position: - continue +# roll_dict = {} +# for symbol in self.pm.symbol_list: +# strike_list = [] +# position = self.pm.current_positions[symbol] +# for signal_id, current_position in position.items(): +# if 'position' not in current_position: +# continue - logger.info(f"Checking Position {current_position['position']['trade_id']} for Moneyness on {date}") - id = current_position['position']['trade_id'] - try: - entry_date = self.pm.trades_map[id].entry_date - except Exception as e: - logger.error(f"Error getting entry date for position {id}: {e}") - entry_date = date - spot = self.chain_spot_timeseries[symbol][date] ## Use the spot price on the date (from chain cause of splits) +# logger.info(f"Checking Position {current_position['position']['trade_id']} for Moneyness on {date}") +# id = current_position['position']['trade_id'] +# try: +# entry_date = self.pm.trades_map[id].entry_date +# except Exception as e: +# logger.error(f"Error getting entry date for position {id}: {e}") +# entry_date = date +# spot = self.chain_spot_timeseries[symbol][date] ## Use the spot price on the date (from chain cause of splits) - if 'long' in current_position['position']: - for option_id in current_position['position']['long']: - option_meta = self.adjust_for_events(entry_date, date, parse_option_tick(option_id)) - strike_list.append(option_meta['strike']/spot if option_meta['put_call'] == 'P' else spot/option_meta['strike']) - - if 'short' in current_position['position']: - for option_id in current_position['position']['short']: - option_meta = self.adjust_for_events(entry_date, date, parse_option_tick(option_id)) - strike_list.append(option_meta['strike']/spot if option_meta['put_call'] == 'P' else spot/option_meta['strike']) +# if 'long' in current_position['position']: +# for option_id in current_position['position']['long']: +# option_meta = self.adjust_for_events(entry_date, date, parse_option_tick(option_id)) +# strike_list.append(option_meta['strike']/spot if option_meta['put_call'] == 'P' else spot/option_meta['strike']) + +# if 'short' in current_position['position']: +# for option_id in current_position['position']['short']: +# option_meta = self.adjust_for_events(entry_date, date, parse_option_tick(option_id)) +# strike_list.append(option_meta['strike']/spot if option_meta['put_call'] == 'P' else spot/option_meta['strike']) - logger.info(f"{id} moneyness list {strike_list}, spot: {spot}, date: {date}, entry_date: {entry_date}") - logger.info(f"{id} moneyness bool list {[x > self.max_moneyness for x in strike_list]}") +# logger.info(f"{id} moneyness list {strike_list}, spot: {spot}, date: {date}, entry_date: {entry_date}") +# logger.info(f"{id} moneyness bool list {[x > self.max_moneyness for x in strike_list]}") - roll_dict[id] = EventTypes.ROLL.value if any([x > self.max_moneyness for x in strike_list]) else EventTypes.HOLD.value - return roll_dict - - def hedge_check(self, - hedge_func: callable, - hedge_args: list, - hedge_kwargs: dict, - ) -> dict: - """ - Responsible for checking if the hedge is needed and if so, queueing in analyze_position - Hedge function should allow 1st argument to be Risk Manager and 2nd argument to be Portfolio Manager - Expected return type is: List[HEDGE]. Where HEDGE is a subclass of RMAction - - params: - hedge_func: callable: function to be called for the hedge - hedge_args: list: arguments to be passed to the hedge function - hedge_kwargs: dict: keyword arguments to be passed to the hedge function - - returns: - dict: dictionary of the hedge actions - """ - pass - - ## Lazy Loading Spot Data - def generate_data(self, symbol): - stk = self.pm.get_underlier_data(symbol) ## Performance isn't affected because of singletons in stock class - if symbol not in self.spot_timeseries: - self.spot_timeseries[symbol] = stk.spot( - ts = True, - ts_start = pd.to_datetime(self.start_date) - BDay(30), - ts_end = pd.to_datetime(self.end_date), - )[self.price_on] - - if symbol not in self.chain_spot_timeseries: - self.chain_spot_timeseries[symbol] = stk.spot( - ts = True, - spot_type = OptionModelAttributes.spot_type.value, - ts_start = pd.to_datetime(self.start_date) - BDay(30), - ts_end = pd.to_datetime(self.end_date), - )[self.price_on] +# roll_dict[id] = EventTypes.ROLL.value if any([x > self.max_moneyness for x in strike_list]) else EventTypes.HOLD.value +# return roll_dict + +# def hedge_check(self, +# hedge_func: callable, +# hedge_args: list, +# hedge_kwargs: dict, +# ) -> dict: +# """ +# Responsible for checking if the hedge is needed and if so, queueing in analyze_position +# Hedge function should allow 1st argument to be Risk Manager and 2nd argument to be Portfolio Manager +# Expected return type is: List[HEDGE]. Where HEDGE is a subclass of RMAction + +# params: +# hedge_func: callable: function to be called for the hedge +# hedge_args: list: arguments to be passed to the hedge function +# hedge_kwargs: dict: keyword arguments to be passed to the hedge function + +# returns: +# dict: dictionary of the hedge actions +# """ +# pass + +# ## Lazy Loading Spot Data +# def generate_data(self, symbol): +# stk = self.pm.get_underlier_data(symbol) ## Performance isn't affected because of singletons in stock class +# if symbol not in self.spot_timeseries: +# self.spot_timeseries[symbol] = stk.spot( +# ts = True, +# ts_start = pd.to_datetime(self.start_date) - BDay(30), +# ts_end = pd.to_datetime(self.end_date), +# )[self.price_on] + +# if symbol not in self.chain_spot_timeseries: +# self.chain_spot_timeseries[symbol] = stk.spot( +# ts = True, +# spot_type = OptionModelAttributes.spot_type.value, +# ts_start = pd.to_datetime(self.start_date) - BDay(30), +# ts_end = pd.to_datetime(self.end_date), +# )[self.price_on] - if symbol not in self.dividend_timeseries: - divs = stk.div_yield_history(start = pd.to_datetime(self.start_date) - BDay(30)) - if not isinstance(divs, (pd.DataFrame, pd.Series)): ## When a ticker has no dividends, it returns None/0 - divs = pd.Series(divs, index = self.spot_timeseries[symbol].index) - self.dividend_timeseries[symbol] = divs +# if symbol not in self.dividend_timeseries: +# divs = stk.div_yield_history(start = pd.to_datetime(self.start_date) - BDay(30)) +# if not isinstance(divs, (pd.DataFrame, pd.Series)): ## When a ticker has no dividends, it returns None/0 +# divs = pd.Series(divs, index = self.spot_timeseries[symbol].index) +# self.dividend_timeseries[symbol] = divs - def parse_position_id(self, positionID): - return parse_position_id(positionID) +# def parse_position_id(self, positionID): +# return parse_position_id(positionID) - def get_position_dict(self, positionID): - return self.parse_position_id(positionID)[0] +# def get_position_dict(self, positionID): +# return self.parse_position_id(positionID)[0] - def get_position_list(self, positionID): - return self.parse_position_id(positionID)[1] +# def get_position_list(self, positionID): +# return self.parse_position_id(positionID)[1] - def get_option_price(self, optID, date): - portfolio = self.pm - return portfolio.options_data[optID][self.option_price][date] +# def get_option_price(self, optID, date): +# portfolio = self.pm +# return portfolio.options_data[optID][self.option_price][date] - def adjust_for_events( - self, - start: str, - date: str, - option: str|dict, - ): - """ - Adjusts the option tick for events like splits or dividends. - """ - if isinstance(option, str): - meta = parse_option_tick(option) - elif isinstance(option, dict): - meta = option - else: - raise ValueError("Option must be a string or a dictionary.") - split = self.splits.get(swap_ticker(meta['ticker']), None) - if split is None: - return meta - for pack in split: - if compare_dates.is_before(start, pack[0]) and compare_dates.is_after(date, pack[0]): - meta['strike'] /= pack[1] - return meta +# def adjust_for_events( +# self, +# start: str, +# date: str, +# option: str|dict, +# ): +# """ +# Adjusts the option tick for events like splits or dividends. +# """ +# if isinstance(option, str): +# meta = parse_option_tick(option) +# elif isinstance(option, dict): +# meta = option +# else: +# raise ValueError("Option must be a string or a dictionary.") +# split = self.splits.get(swap_ticker(meta['ticker']), None) +# if split is None: +# return meta +# for pack in split: +# if compare_dates.is_before(start, pack[0]) and compare_dates.is_after(date, pack[0]): +# meta['strike'] /= pack[1] +# return meta diff --git a/EventDriven/riskmanager_decomm.py b/EventDriven/riskmanager/.decomm/riskmanager_decomm.py similarity index 100% rename from EventDriven/riskmanager_decomm.py rename to EventDriven/riskmanager/.decomm/riskmanager_decomm.py diff --git a/EventDriven/riskmanager/_order_validator.py b/EventDriven/riskmanager/_order_validator.py index b6dac66..4b13553 100644 --- a/EventDriven/riskmanager/_order_validator.py +++ b/EventDriven/riskmanager/_order_validator.py @@ -73,7 +73,7 @@ from abc import ABC, abstractmethod from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.picker import STRATEGY_MAP -from EventDriven.riskmanager.market_data import get_timeseries_obj, OPTION_TIMESERIES_START_DATE +from trade.datamanager.vars import get_times_series from EventDriven.riskmanager.picker import OrderSchema logger = setup_logger("EventDriven.riskmanager._order_validator", stream_log_level="WARNING") @@ -258,8 +258,8 @@ def build_inputs_with_config( else: raise ValueError("Invalid option type. Must be 'C' or 'P'.") - timeseries = get_timeseries_obj() - timeseries.load_timeseries(tick, OPTION_TIMESERIES_START_DATE, datetime.now()) + timeseries = get_times_series() + timeseries.load_timeseries(tick, end_date= datetime.now()) ## Get spot price for the tick at the date. chain_spot is used for option pricing spot = timeseries.get_at_index(tick, date).chain_spot.close diff --git a/EventDriven/riskmanager/market_data.py b/EventDriven/riskmanager/market_data.py index 44fcf83..b0eb2f4 100644 --- a/EventDriven/riskmanager/market_data.py +++ b/EventDriven/riskmanager/market_data.py @@ -205,10 +205,10 @@ from trade.assets.rates import get_risk_free_rate_helper from EventDriven._vars import OPTION_TIMESERIES_START_DATE, load_riskmanager_cache from EventDriven.exceptions import UnaccessiblePropertyError -from trade import register_signal - - -logger = setup_logger("EventDriven.riskmanager.market_data", stream_log_level="WARNING") +from trade import register_signal, SIGNALS_TO_RUN +raise DeprecationWarning("This module is deprecated. Refer to `trade.datamanager.market_data` instead.") +logger = setup_logger("EventDriven.riskmanager.market_data", stream_log_level="INFO") +logger.critical("Market data from EventDriven.riskmanager.market_data. This module is deprecated. Refer to `trade.datamanager.market_data` instead.") ## TODO: This var is from optionlib. Once ready, import from there. ## TODO: Implement interval handling to have multiple intervals @@ -217,6 +217,8 @@ DIVIDEND_CACHE: CustomCache = load_riskmanager_cache(target="dividend_timeseries") SPOT_CACHE: CustomCache = load_riskmanager_cache(target="spot_timeseries") CHAIN_SPOT_CACHE: CustomCache = load_riskmanager_cache(target="chain_spot_timeseries") +SPLIT_FACTOR_CACHE: CustomCache = load_riskmanager_cache(target="split_factor_timeseries", create_on_missing=True, clear_on_exit=False) +_SANITIZED_ON_EXIT: bool = False @dataclass @@ -228,8 +230,9 @@ class AtIndexResult: spot: pd.Series chain_spot: pd.Series rates: pd.Series - dividends: pd.Series - dividend_yield: pd.Series + dividends: int | float + dividend_yield: int | float + split_factor: float | int additional: Dict[str, Any] = field(default_factory=dict) def __repr__(self) -> str: @@ -244,6 +247,8 @@ class TimeseriesData: chain_spot: pd.DataFrame dividends: pd.Series dividend_yield: pd.Series + split_factor: pd.Series + rates: Optional[pd.Series] = None additional_data: Dict[str, pd.Series] = field(default_factory=dict) def __repr__(self) -> str: @@ -256,7 +261,7 @@ class MarketTimeseries: additional_data: Dict[str, Any] = field(default_factory=dict) rates: pd.DataFrame = field(default_factory=get_risk_free_rate_helper) - DEFAULT_NAMES: ClassVar[List[str]] = ["spot", "chain_spot", "dividends"] + DEFAULT_NAMES: ClassVar[List[str]] = ["spot", "chain_spot", "dividends", "split_factor", "dividend_yield"] _refresh_delta: Optional[timedelta] = timedelta(minutes=30) _last_refresh: Optional[datetime] = field(default_factory=ny_now) _start: str = OPTION_TIMESERIES_START_DATE @@ -274,6 +279,12 @@ def spot(self) -> dict: "The 'spot' property is not accessible directly. Use 'get_timeseries' method instead. Or access via 'get_at_index' method." ) + @property + def split_factor(self) -> dict: + raise UnaccessiblePropertyError( + "The 'split_factor' property is not accessible directly. Use 'get_timeseries' method instead. Or access via 'get_at_index' method." + ) + @property def chain_spot(self) -> dict: raise UnaccessiblePropertyError( @@ -297,17 +308,26 @@ def _chain_spot(self) -> CustomCache: @property def _dividends(self) -> CustomCache: return DIVIDEND_CACHE - + + @property + def _split_factor(self) -> CustomCache: + return SPLIT_FACTOR_CACHE + @classmethod def clear_caches(cls): """Clear all caches used by MarketTimeseries.""" SPOT_CACHE.clear() CHAIN_SPOT_CACHE.clear() DIVIDEND_CACHE.clear() + SPLIT_FACTOR_CACHE.clear() logger.info("All MarketTimeseries caches have been cleared.") + @timeit def _on_exit_sanitize(self): """Remove today's data from all stored timeseries data.""" + global _SANITIZED_ON_EXIT + if _SANITIZED_ON_EXIT: + return try: def _check_instance(d): @@ -352,11 +372,26 @@ def _check_instance(d): d = d[d.index < self._today] self._dividends[sym] = d - logger.info("Successfully sanitized timeseries data on exit.") + for sym in self._split_factor.keys(): + d = self._split_factor[sym] + if not _check_instance(d): + logger.critical( + "Data for symbol %s in split_factor cache is not a DataFrame or Series. Skipping sanitization. Data: %s", + sym, + d, + ) + del self._split_factor[sym] + continue + + d = d[d.index < self._today] + self._split_factor[sym] = d + + logger.info("Sanitization of today's data on exit completed successfully.") + _SANITIZED_ON_EXIT = True except Exception as e: logger.error("Error during sanitization: %s", e, exc_info=True) - @timeit + # @timeit def _already_loaded( self, sym: str, interval: str = "1d", start: str | datetime = None, end: str | datetime = None ) -> Tuple[bool, List[pd.Timestamp]]: @@ -369,18 +404,31 @@ def _already_loaded( sym_available = sym in self._spot all_dates_present = False - data_to_check = [self._spot.get(sym), self._chain_spot.get(sym), self._dividends.get(sym)] + data_to_check = [ + (self._spot.get(sym), "spot"), + (self._chain_spot.get(sym), "chain_spot"), + (self._dividends.get(sym), "dividends"), + (self._split_factor.get(sym), "split_factor"), + ] missing_dates_set = set() all_dates_present = False - for data in data_to_check: + for data, data_type in data_to_check: # noqa if data is not None: missing_dates = get_missing_dates(data, start, end) missing_dates_set.update(missing_dates) - if not missing_dates: - all_dates_present = True - else: - all_dates_present = False + + else: + missing_dates = pd.bdate_range(start=start, end=end).to_pydatetime().tolist() + missing_dates_set.update(missing_dates) + all_dates_present = False + + ## If no missing dates, all dates present + if not missing_dates_set: + all_dates_present = True + else: + all_dates_present = False + ## If all dates not present, return missing dates return_dates = list(missing_dates_set) if not all_dates_present: @@ -395,7 +443,7 @@ def _already_loaded( return_dates = [] return (sym_available and all_dates_present), return_dates - + def cache_it(self, timeseries: TimeseriesData, sym: str) -> None: """ Cache the provided timeseries data for the given symbol. @@ -404,10 +452,11 @@ def cache_it(self, timeseries: TimeseriesData, sym: str) -> None: spot = timeseries.spot.copy() chain_spot = timeseries.chain_spot.copy() dividends = timeseries.dividends.copy() - + split_factor = timeseries.split_factor.copy() self._spot[sym] = self._remove_today_data(spot) self._chain_spot[sym] = self._remove_today_data(chain_spot) self._dividends[sym] = self._remove_today_data(dividends) + self._split_factor[sym] = self._remove_today_data(split_factor) logger.info("Cached timeseries data for symbol %s", sym) def already_loaded( @@ -431,15 +480,26 @@ def _remove_today_data(self, data: pd.DataFrame | pd.Series) -> pd.DataFrame | p raise ValueError("Data must be a pandas DataFrame or Series. Got type: {}".format(type(data))) @timeit - def _sanitize_today_data(self) -> None: + def _sanitize_today_data(self, force_after_eod: bool = False) -> None: """Remove today's data from all stored timeseries data.""" - + current_time = ny_now() + if not force_after_eod and current_time.hour > 18: + logger.info("Current time is after 6 PM NY time. Skipping sanitization of today's data.") + return + + logger.info("Sanitizing today's data from all stored timeseries data...") for sym in self._spot.keys(): self._spot[sym] = self._remove_today_data(self._spot[sym]) for sym in self._chain_spot.keys(): self._chain_spot[sym] = self._remove_today_data(self._chain_spot[sym]) - for sym in self._dividends.keys(): - self._dividends[sym] = self._remove_today_data(self._dividends[sym]) + + ## No need to sanitize dividends often. + # for sym in self._dividends.keys(): + # self._dividends[sym] = self._remove_today_data(self._dividends[sym]) + + ## No need to sanitize split factor often. + # for sym in self._split_factor.keys(): + # self._split_factor[sym] = self._remove_today_data(self._split_factor[sym]) @timeit def _sanitize_data(self) -> None: @@ -476,6 +536,39 @@ def _sanitize_data(self) -> None: data.dropna(how="all", inplace=True) self._dividends[sym] = data + for sym in self._split_factor.keys(): + sym = sym.upper() + data = self._split_factor[sym] + data.index = pd.to_datetime(data.index) + data = data[~data.index.duplicated(keep="last")] + data = data.sort_index() + data.dropna(how="all", inplace=True) + self._split_factor[sym] = data + + def get_split_factor_at_index(self, sym: str, index: pd.Timestamp) -> float | int: + """ + Retrieve the split factor for a given symbol at a specific index (date). + Args: + sym (str): The stock symbol. + index (pd.Timestamp or str): The date for which to retrieve the split factor. + Returns: + float | int: The split factor at the specified date. + """ + split_factor_series = self._split_factor.get(sym) + if split_factor_series is None: + return 1.0 + + index = pd.to_datetime(index) + if index in split_factor_series.index: + return split_factor_series.loc[index] + else: + prior_dates = split_factor_series.index[split_factor_series.index <= index] + if not prior_dates.empty: + nearest_date = prior_dates.max() + return split_factor_series.loc[nearest_date] + else: + return 1.0 + def _pre_sanitize_load_timeseries( self, sym: str, @@ -518,15 +611,24 @@ def _pre_sanitize_load_timeseries( logger.error("Failed to retrieve dividends for symbol %s", sym) divs = pd.DataFrame({"amount": [0]}, index=pd.bdate_range(start=self._start, end=self._end, freq=interval)) + try: + split_factor = chain_spot["split_factor"] + except Exception: + logger.error("Failed to retrieve split factor for symbol %s", sym) + split_factor = pd.Series(1, index=pd.bdate_range(start=self._start, end=self._end, freq=interval)) + ## Ensure datetime index divs.index = pd.to_datetime(divs.index) - divs = divs.reindex(pd.bdate_range(start=self._start, end=self._end, freq=interval), method="ffill") + use_start = min(spot.index.min(), chain_spot.index.min(), divs.index.min()) + use_end = max(spot.index.max(), chain_spot.index.max(), divs.index.max()) + divs = divs.reindex(pd.bdate_range(start=use_start, end=use_end, freq=interval), method="ffill") divs = resample(divs["amount"], method="ffill", interval=interval) ## Current Data current_spot = self._spot.get(sym) current_chain_spot = self._chain_spot.get(sym) current_divs = self._dividends.get(sym) + current_split_factor = self._split_factor.get(sym) ## We are moving from overwritting prev data to merging new data if current_spot is not None: @@ -540,6 +642,9 @@ def _pre_sanitize_load_timeseries( if current_divs is not None: divs = pd.concat([current_divs, divs]).sort_index() divs = divs[~divs.index.duplicated(keep="last")] + if current_split_factor is not None: + split_factor = pd.concat([current_split_factor, split_factor]).sort_index() + split_factor = split_factor[~split_factor.index.duplicated(keep="last")] ## Assign data directly to cache ## We remove today's data to avoid situations where it was loaded intraday and remains in database @@ -547,6 +652,7 @@ def _pre_sanitize_load_timeseries( self._spot[sym] = spot self._chain_spot[sym] = chain_spot self._dividends[sym] = divs + self._split_factor[sym] = split_factor def load_timeseries( self, @@ -571,7 +677,12 @@ def _is_date_in_index(self, sym: str, date: pd.Timestamp, interval: str = "1d") Returns: bool: True if the date is present, False otherwise. """ - all_data = [self._spot.get(sym), self._chain_spot.get(sym), self._dividends.get(sym)] + all_data = [ + self._spot.get(sym), + self._chain_spot.get(sym), + self._dividends.get(sym), + self._split_factor.get(sym), + ] for data in all_data: date = pd.to_datetime(date).date() @@ -611,15 +722,25 @@ def get_at_index(self, sym: str, index: pd.Timestamp, interval: str = "1d") -> A spot = self._spot[sym].loc[index_str] if sym in self._spot else None chain_spot = self._chain_spot[sym].loc[index_str] if sym in self._chain_spot else None dividends = self._dividends[sym].loc[index_str] if sym in self._dividends else None - rates = self.rates.loc[index_str] if self.rates is not None else None + rates = None dividend_yield = dividends / spot["close"] if spot is not None and dividends is not None else None + split_factor = self._split_factor[sym].loc[index_str] if sym in self._split_factor else None + self._sanitize_today_data() + return AtIndexResult( - spot=spot, chain_spot=chain_spot, dividends=dividends, sym=sym, date=index_str, rates=rates, dividend_yield=dividend_yield + spot=spot, + chain_spot=chain_spot, + dividends=dividends, + sym=sym, + date=index_str, + rates=rates, + dividend_yield=dividend_yield, + split_factor=split_factor, ) def calculate_additional_data( self, - factor: Literal["spot", "chain_spot", "dividends"], + factor: Literal["spot", "chain_spot", "dividends", "split_factor"], sym: str, additional_data_name: str, _callable: Any, @@ -627,7 +748,7 @@ def calculate_additional_data( force_add: bool = False, ) -> None: """ - Load additional data for a given factor (spot, chain_spot, dividends) using a callable function. + Load additional data for a given factor (spot, chain_spot, dividends, split_factor) using a callable function. Process: Callable passed should only take in a pd.Series and return a pd.Series. @@ -635,7 +756,7 @@ def calculate_additional_data( The schema of additional_data: {additional_data_name: {sym: pd.Series}} Args: - factor (Literal['spot', 'chain_spot', 'dividends']): The factor to process. + factor (Literal['spot', 'chain_spot', 'dividends', 'split_factor']): The factor to process. sym (str): The stock symbol. additional_data_name (str): The name under which to store the additional data. _callable (Any): A callable function that processes the pd.Series. @@ -683,25 +804,56 @@ def calculate_additional_data( def get_timeseries( self, sym: str, - factor: Literal["spot", "chain_spot", "dividends", "additional"] = None, + factor: Literal["spot", "chain_spot", "dividends", "split_factor", "additional"] = None, interval: str = "1d", additional_data_name: Optional[str] = None, start_date: str | datetime = None, end_date: str | datetime = None, + skip_preload_check: bool = False, ) -> TimeseriesData: """ Retrieve the timeseries data for a given symbol and factor. Args: sym (str): The stock symbol. - factor (Literal['spot', 'chain_spot', 'dividends', 'additional']): The factor to retrieve. + factor (Literal['spot', 'chain_spot', 'dividends', 'split_factor', 'additional']): The factor to retrieve. additional_data_name (Optional[str]): The name of the additional data if factor is 'additional'. Returns: TimeseriesData: A dataclass containing the requested timeseries data. """ sym = sym.upper() - if not self.already_loaded(sym, interval): + must_preload = False + end_date = end_date or self._end + + ## Adding `must_preload`. This will be determined based on if + ## 1. Today's date is in end_date + ## 2. Current time is before market close + if pd.to_datetime(end_date).date() >= ny_now().date(): + current_time = ny_now() + if current_time.hour < 20: + must_preload = True + + if must_preload: + logger.warning( + "End date %s is today or in the future and current time is before market close. Forcing preload check.", + end_date, + ) + else: + logger.warning( + "End date %s is in the past or current time is after market close. Preload check will be skipped if specified.", + end_date, + ) + + + ## Check if data is already loaded + if skip_preload_check and not must_preload: + already_loaded = True + else: + already_loaded, _ = self._already_loaded(sym, interval, start_date, end_date) + + if not already_loaded: logger.critical("Timeseries for symbol %s not loaded. Loading now.", sym) self._pre_sanitize_load_timeseries(sym, interval=interval, force=True) + if factor not in self.DEFAULT_NAMES + ["additional", None]: raise ValueError(f"Factor {factor} not recognized. Must be one of {self.DEFAULT_NAMES + ['additional']}.") if factor == "additional": @@ -711,12 +863,17 @@ def get_timeseries( if data is None: raise ValueError(f"No additional data found for name {additional_data_name} and symbol {sym}.") return TimeseriesData( - spot=None, chain_spot=None, dividends=None, additional_data={additional_data_name: data} + spot=None, + chain_spot=None, + dividends=None, + additional_data={additional_data_name: data}, + split_factor=None, + dividend_yield=None, ) elif factor in self.DEFAULT_NAMES: factor = "_" + factor - if factor in ["_spot", "_chain_spot", "_dividends"]: + if factor in ["_spot", "_chain_spot", "_dividends", "_split_factor"]: data = getattr(self, factor).get(sym) elif factor == "_dividend_yield": divs = self._dividends.get(sym) @@ -737,13 +894,15 @@ def get_timeseries( if data is None: raise ValueError(f"No data found for factor {factor} and symbol {sym}.") if factor == "_spot": - ts = TimeseriesData(spot=data, chain_spot=None, dividends=None) + ts = TimeseriesData(spot=data, chain_spot=None, dividends=None, dividend_yield=None, split_factor=None) elif factor == "_chain_spot": - ts = TimeseriesData(spot=None, chain_spot=data, dividends=None) + ts = TimeseriesData(spot=None, chain_spot=data, dividends=None, dividend_yield=None, split_factor=None) elif factor == "_dividends": - ts = TimeseriesData(spot=None, chain_spot=None, dividends=data) + ts = TimeseriesData(spot=None, chain_spot=None, dividends=data, dividend_yield=None, split_factor=None) elif factor == "_dividend_yield": - ts = TimeseriesData(spot=None, chain_spot=None, dividends=None, dividend_yield=data) + ts = TimeseriesData(spot=None, chain_spot=None, dividends=None, dividend_yield=data, split_factor=None) + elif factor == "_split_factor": + ts = TimeseriesData(spot=None, chain_spot=None, dividends=None, split_factor=data, dividend_yield=None) else: raise ValueError(f"Unhandled factor {factor}.") @@ -752,6 +911,7 @@ def get_timeseries( chain_spot = self._chain_spot.get(sym) dividends = self._dividends.get(sym) dividend_yield = dividends / spot["close"] if spot is not None and dividends is not None else None + split_factor = self._split_factor.get(sym) if start_date is not None or end_date is not None: start_date = pd.to_datetime(start_date).strftime("%Y-%m-%d") if start_date is not None else None end_date = pd.to_datetime(end_date).strftime("%Y-%m-%d") if end_date is not None else None @@ -759,12 +919,23 @@ def get_timeseries( spot = spot[spot.index >= start_date] chain_spot = chain_spot[chain_spot.index >= start_date] dividends = dividends[dividends.index >= start_date] + dividend_yield = dividend_yield[dividend_yield.index >= start_date] + split_factor = split_factor[split_factor.index >= start_date] if end_date is not None: spot = spot[spot.index <= end_date] chain_spot = chain_spot[chain_spot.index <= end_date] dividends = dividends[dividends.index <= end_date] dividend_yield = dividend_yield[dividend_yield.index <= end_date] - ts = TimeseriesData(spot=spot, chain_spot=chain_spot, dividends=dividends, dividend_yield=dividend_yield) + split_factor = split_factor[split_factor.index <= end_date] + ts = TimeseriesData( + spot=spot, + chain_spot=chain_spot, + dividends=dividends, + dividend_yield=dividend_yield, + split_factor=split_factor, + rates=self.rates["annualized"], + ) + self._sanitize_today_data() return ts @@ -772,10 +943,10 @@ def __repr__(self) -> str: return f"MarketTimeseries(symbols: {list(self._spot.keys())}, intervals: {list(self._spot.keys())})" -def get_timeseries_obj() -> MarketTimeseries: +def get_timeseries_obj(live: bool = False) -> MarketTimeseries: global OPTIMESERIES if OPTIMESERIES is None: - OPTIMESERIES = MarketTimeseries() + OPTIMESERIES = MarketTimeseries(_end = (datetime.now() - BDay(1)).strftime("%Y-%m-%d") if not live else datetime.now().strftime("%Y-%m-%d")) return OPTIMESERIES @@ -783,3 +954,11 @@ def get_timeseries_obj() -> MarketTimeseries: def reset_timeseries_obj() -> None: global OPTIMESERIES OPTIMESERIES = None + + +if __name__ == "__main__": + mts = get_timeseries_obj() + mts.load_timeseries("BA", force=True) + ts = mts.get_timeseries("BA") + print(ts) + print(SIGNALS_TO_RUN) diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 43e0976..0b77d80 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -146,13 +146,14 @@ ## Options Timeseries class for handling data retrieval from datetime import datetime from dateutil.relativedelta import relativedelta -from EventDriven.riskmanager.market_data import MarketTimeseries +from trade.datamanager.market_data import MarketTimeseries from EventDriven._vars import load_riskmanager_cache, ADD_COLUMNS_FACTORY from EventDriven.riskmanager.utils import ( parse_position_id, swap_ticker, - load_position_data, + load_position_data, # noqa add_skip_columns, + load_position_data_new ) from trade.helpers.decorators import timeit from trade.helpers.threads import runThreads @@ -323,7 +324,7 @@ def calculate_option_data(self, position_id: str, date: Union[datetime, str]) -> for p in position_dict.values(): for s in p: ticker = swap_ticker(s["ticker"]) - self.market_timeseries.load_timeseries(sym=ticker, interval=self.undl_timeseries_config.interval) + self.market_timeseries.load_timeseries(sym=ticker) timeseries_data = self.market_timeseries.get_timeseries(sym=ticker) @timeit @@ -444,7 +445,7 @@ def _skip_columns_adjustment( return position_data # self.position_data[position_id] = position_data - def load_position_data(self, opttick) -> pd.DataFrame: + def load_position_data(self, opttick) -> pd.DataFrame: # noqa """ Load position data for a given option tick. @@ -453,18 +454,23 @@ def load_position_data(self, opttick) -> pd.DataFrame: """ ## Get Meta meta = parse_option_tick(opttick) - self.market_timeseries.load_timeseries(sym=meta["ticker"], interval=self.undl_timeseries_config.interval) - timeseries_data = self.market_timeseries.get_timeseries(sym=meta["ticker"]) - return load_position_data( - opttick, - self.options_cache, - self.start_date, - self.end_date, - s=timeseries_data.chain_spot["close"], - r=self.rf_timeseries, - y=timeseries_data.dividends, - s0_close=timeseries_data.spot["close"], - ) + self.market_timeseries.load_timeseries(sym=meta["ticker"]) + # timeseries_data = self.market_timeseries.get_timeseries(sym=meta["ticker"]) + # return load_position_data( + # opttick, + # self.options_cache, + # self.start_date, + # self.end_date, + # s=timeseries_data.chain_spot["close"], + # r=self.rf_timeseries, + # y=timeseries_data.dividends, + # s0_close=timeseries_data.spot["close"], + # ) + return load_position_data_new( + opttick=opttick, + processed_option_data=self.options_cache, + start=self.start_date, + end=self.end_date) def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: """ diff --git a/EventDriven/riskmanager/notebooks/switchout_datasource.ipynb b/EventDriven/riskmanager/notebooks/switchout_datasource.ipynb new file mode 100644 index 0000000..31c8431 --- /dev/null +++ b/EventDriven/riskmanager/notebooks/switchout_datasource.ipynb @@ -0,0 +1,1217 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 9, + "id": "d411aa9c", + "metadata": {}, + "outputs": [], + "source": [ + "from EventDriven.riskmanager.utils import new_generate_spot_greeks, old_generate_spot_greeks, load_position_data_new, load_position_data\n", + "from trade.datamanager import BaseDataManager\n", + "from trade.datamanager._enums import OptionPricingModel, DivType\n", + "import pandas as pd\n", + "pd.options.plotting.backend = \"plotly\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "798d5adf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for DividendDataManager (CACHE_NAME='dividend_data_manager')\n", + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for RatesDataManager (CACHE_NAME='rates_data_manager')\n", + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for ForwardDataManager (CACHE_NAME='forward_data_manager')\n", + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for OptionSpotDataManager (CACHE_NAME='option_spot_manager')\n", + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for SpotDataManager (CACHE_NAME='spot_data_manager')\n", + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for VolDataManager (CACHE_NAME='vol_data_manager_cache')\n", + "2026-02-08 22:46:26 [test] trade.datamanager.base INFO: Clearing cache for GreekDataManager (CACHE_NAME='greek_datamanager_cache')\n" + ] + } + ], + "source": [ + "BaseDataManager.clear_all_caches()\n", + "BaseDataManager.CONFIG.option_model = OptionPricingModel.BINOMIAL\n", + "BaseDataManager.CONFIG.dividend_type = DivType.CONTINUOUS" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3c4e946a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fetching rates data from yfinance directly during market hours\n", + "2026-02-08 22:46:27 [test] DataManager.py CRITICAL: Extra Cols not implemented for BS Greeks\n", + "Fetching rates data from yfinance directly during market hours\n" + ] + } + ], + "source": [ + "old_data = old_generate_spot_greeks(\n", + " opttick=\"AAPL20260918C260\",\n", + " start_date=\"2024-01-01\",\n", + " end_date=\"2026-02-07\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "1b3c0c92", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
VegaVannaVolgaDeltaGammaThetaRhoMidpointCloseaskClosebid
Datetime
2025-05-060.7795170.0112471.4846910.2823450.005537-0.0250900.6392409.20010.907.50
2025-05-070.7636610.0111311.4866470.2776540.005413-0.0251170.6180889.10010.257.95
2025-05-080.7863370.0106771.3294890.2920100.005368-0.0265780.64778810.00010.159.85
2025-05-090.7885310.0108341.3616280.2910800.005450-0.0262640.6504899.8259.959.70
2025-05-120.9023000.0096770.8991010.3498080.005799-0.0299000.82233912.82512.9512.70
.................................
2026-02-020.791312-0.0021940.1760700.6450190.006386-0.0595100.89562930.77530.9030.65
2026-02-030.791608-0.0019320.1493820.6404770.006299-0.0607150.88134430.87531.1530.60
2026-02-040.772046-0.0032580.3394730.6824420.005806-0.0611890.94737635.67536.1535.20
2026-02-050.775989-0.0027950.2778850.6753060.005641-0.0634330.92568436.15036.3535.95
2026-02-060.763374-0.0035290.3881870.6916080.005704-0.0614590.95486636.75036.9536.55
\n", + "

199 rows × 10 columns

\n", + "
" + ], + "text/plain": [ + " Vega Vanna Volga Delta Gamma Theta \\\n", + "Datetime \n", + "2025-05-06 0.779517 0.011247 1.484691 0.282345 0.005537 -0.025090 \n", + "2025-05-07 0.763661 0.011131 1.486647 0.277654 0.005413 -0.025117 \n", + "2025-05-08 0.786337 0.010677 1.329489 0.292010 0.005368 -0.026578 \n", + "2025-05-09 0.788531 0.010834 1.361628 0.291080 0.005450 -0.026264 \n", + "2025-05-12 0.902300 0.009677 0.899101 0.349808 0.005799 -0.029900 \n", + "... ... ... ... ... ... ... \n", + "2026-02-02 0.791312 -0.002194 0.176070 0.645019 0.006386 -0.059510 \n", + "2026-02-03 0.791608 -0.001932 0.149382 0.640477 0.006299 -0.060715 \n", + "2026-02-04 0.772046 -0.003258 0.339473 0.682442 0.005806 -0.061189 \n", + "2026-02-05 0.775989 -0.002795 0.277885 0.675306 0.005641 -0.063433 \n", + "2026-02-06 0.763374 -0.003529 0.388187 0.691608 0.005704 -0.061459 \n", + "\n", + " Rho Midpoint Closeask Closebid \n", + "Datetime \n", + "2025-05-06 0.639240 9.200 10.90 7.50 \n", + "2025-05-07 0.618088 9.100 10.25 7.95 \n", + "2025-05-08 0.647788 10.000 10.15 9.85 \n", + "2025-05-09 0.650489 9.825 9.95 9.70 \n", + "2025-05-12 0.822339 12.825 12.95 12.70 \n", + "... ... ... ... ... \n", + "2026-02-02 0.895629 30.775 30.90 30.65 \n", + "2026-02-03 0.881344 30.875 31.15 30.60 \n", + "2026-02-04 0.947376 35.675 36.15 35.20 \n", + "2026-02-05 0.925684 36.150 36.35 35.95 \n", + "2026-02-06 0.954866 36.750 36.95 36.55 \n", + "\n", + "[199 rows x 10 columns]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "old_data\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "3c8177f7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-08 22:46:37 [test] trade.datamanager.vars INFO: Loading timeseries for AAPL...\n", + "2026-02-08 22:46:37 [test] trade.datamanager.utils INFO: Cutting off today's data for key: AAPL to avoid saving partial day data.\n", + "2026-02-08 22:46:37 [test] trade.datamanager.market_data INFO: Loaded spot data for symbol AAPL into cache.\n", + "2026-02-08 22:46:38 [test] trade.datamanager.utils INFO: Cutting off today's data for key: AAPL to avoid saving partial day data.\n", + "2026-02-08 22:46:38 [test] trade.datamanager.market_data INFO: Loaded chain spot data for symbol AAPL into cache.\n", + "2026-02-08 22:46:38 [test] trade.datamanager.market_data_helpers.dividends INFO: Ticker AAPL found in dividend cache.\n", + "2026-02-08 22:46:39 [test] trade.datamanager.market_data INFO: Loaded dividend data for symbol AAPL into cache.\n", + "2026-02-08 22:46:39 [test] trade.datamanager.utils INFO: Cutting off today's data for key: AAPL to avoid saving partial day data.\n", + "2026-02-08 22:46:39 [test] trade.datamanager.market_data INFO: Loaded chain spot data for symbol AAPL into cache.\n", + "2026-02-08 22:46:39 [test] trade.datamanager.market_data INFO: Loaded split factor data for symbol AAPL into cache.\n", + "2026-02-08 22:46:39 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-08 22:46:39 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-08 22:46:39 [test] trade.datamanager.utils INFO: Sanitizing data from 2017-01-01 to 2026-02-08 22:46:22.730132...\n", + "2026-02-08 22:46:39 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-08 22:46:39 [test] trade.datamanager.utils INFO: Sanitizing data from 2017-01-01 to 2026-02-08 22:46:22.730132...\n", + "2026-02-08 22:46:40 [test] trade.datamanager.rates INFO: No cache found for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching from source.\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D to avoid saving partial day data.\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 to 2026-02-07...\n", + "2026-02-08 22:46:42 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 00:00:00 to 2026-02-07 00:00:00...\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 00:00:00 to 2026-02-07 00:00:00...\n", + "2026-02-08 22:46:42 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 00:00:00 to 2026-02-07 00:00:00...\n", + "2026-02-08 22:46:42 [test] trade.datamanager.utils INFO: Using cached date range for 2024-01-01 00:00:00 - 2026-02-07 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-08 22:46:42 [test] trade.datamanager.option_spot INFO: No cache found for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:260. Fetching from source.\n", + "2026-02-08 22:46:43 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:260 to avoid saving partial day data.\n", + "2026-02-08 22:46:43 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 to 2026-02-07...\n", + "2026-02-08 22:46:43 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-08 22:46:43 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-08 22:46:43 [test] trade.datamanager.utils INFO: Using cached date range for 2024-01-01 00:00:00 - 2026-02-07 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-08 22:46:43 [test] trade.datamanager.utils INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching from source.\n", + "2026-02-08 22:46:43 [test] trade.datamanager.utils INFO: No data requested to load in _load_model_data_timeseries(). Option: Symbol=AAPL, exp=2026-09-18 00:00:00, strike=260.0 right=C Load bools: d=False, r=False, s=False, f=False, opt_spot=False, vol=False, greek=False\n", + "2026-02-08 22:46:48 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-08 22:46:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 to 2026-02-07...\n", + "2026-02-08 22:46:48 [test] trade.datamanager.utils INFO: Using cached date range for 2024-01-01 00:00:00 - 2026-02-07 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-08 22:46:48 [test] trade.datamanager.utils INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching from source.\n", + "2026-02-08 22:46:48 [test] trade.datamanager.utils INFO: No data requested to load in _load_model_data_timeseries(). Option: Symbol=AAPL, exp=2026-09-18 00:00:00, strike=260.0 right=C Load bools: d=False, r=False, s=False, f=False, opt_spot=False, vol=False, greek=False\n", + "2026-02-08 22:46:49 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-08 22:46:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 to 2026-02-07...\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
GammaDeltaThetaRhoVolgaVegaMidpointCloseaskClosebid
datetime
2025-05-060.0056700.284534-0.0266800.645060-0.0098610.7890209.20010.907.50
2025-05-070.0055390.279719-0.0269400.623503-0.0094690.7822449.10010.257.95
2025-05-080.0054940.294235-0.0277110.653668-0.0085280.77465510.00010.159.85
2025-05-090.0055800.293327-0.0274320.656446-0.0089000.7787289.8259.959.70
2025-05-120.0059470.352829-0.0321950.830869-0.0092290.91897512.82512.9512.70
..............................
2026-02-020.0064980.650133-0.0637810.9042950.0010660.80015930.77530.9030.65
2026-02-030.0064080.645413-0.0648730.8896520.0009180.79892830.87531.1530.60
2026-02-040.0059010.687810-0.0650340.9566090.0015130.76688035.67536.1535.20
2026-02-050.0057320.680318-0.0670500.9342470.0012160.76702736.15036.3535.95
2026-02-060.0057940.697090-0.0657670.9642650.0016330.76401936.75036.9536.55
\n", + "

191 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " Gamma Delta Theta Rho Volga Vega \\\n", + "datetime \n", + "2025-05-06 0.005670 0.284534 -0.026680 0.645060 -0.009861 0.789020 \n", + "2025-05-07 0.005539 0.279719 -0.026940 0.623503 -0.009469 0.782244 \n", + "2025-05-08 0.005494 0.294235 -0.027711 0.653668 -0.008528 0.774655 \n", + "2025-05-09 0.005580 0.293327 -0.027432 0.656446 -0.008900 0.778728 \n", + "2025-05-12 0.005947 0.352829 -0.032195 0.830869 -0.009229 0.918975 \n", + "... ... ... ... ... ... ... \n", + "2026-02-02 0.006498 0.650133 -0.063781 0.904295 0.001066 0.800159 \n", + "2026-02-03 0.006408 0.645413 -0.064873 0.889652 0.000918 0.798928 \n", + "2026-02-04 0.005901 0.687810 -0.065034 0.956609 0.001513 0.766880 \n", + "2026-02-05 0.005732 0.680318 -0.067050 0.934247 0.001216 0.767027 \n", + "2026-02-06 0.005794 0.697090 -0.065767 0.964265 0.001633 0.764019 \n", + "\n", + " Midpoint Closeask Closebid \n", + "datetime \n", + "2025-05-06 9.200 10.90 7.50 \n", + "2025-05-07 9.100 10.25 7.95 \n", + "2025-05-08 10.000 10.15 9.85 \n", + "2025-05-09 9.825 9.95 9.70 \n", + "2025-05-12 12.825 12.95 12.70 \n", + "... ... ... ... \n", + "2026-02-02 30.775 30.90 30.65 \n", + "2026-02-03 30.875 31.15 30.60 \n", + "2026-02-04 35.675 36.15 35.20 \n", + "2026-02-05 36.150 36.35 35.95 \n", + "2026-02-06 36.750 36.95 36.55 \n", + "\n", + "[191 rows x 9 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "new_data = new_generate_spot_greeks(\n", + " opttick=\"AAPL20260918C260\",\n", + " start_date=\"2024-01-01\",\n", + " end_date=\"2026-02-07\"\n", + ")\n", + "new_data" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "61bc4137", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'BaseDataManager' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mBaseDataManager\u001b[49m\u001b[38;5;241m.\u001b[39mclear_all_caches()\n\u001b[1;32m 2\u001b[0m load_position_data_new(\n\u001b[1;32m 3\u001b[0m opttick\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mAAPL20260918C260\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 4\u001b[0m processed_option_data\u001b[38;5;241m=\u001b[39m{},\n\u001b[1;32m 5\u001b[0m start\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m2024-01-01\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 6\u001b[0m end\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m2026-02-07\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 7\u001b[0m )\n", + "\u001b[0;31mNameError\u001b[0m: name 'BaseDataManager' is not defined" + ] + } + ], + "source": [ + "BaseDataManager.clear_all_caches()\n", + "load_position_data_new(\n", + " opttick=\"AAPL20260918C260\",\n", + " processed_option_data={},\n", + " start=\"2024-01-01\",\n", + " end=\"2026-02-07\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "f03d933c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-07 14:39:58 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-07 14:39:58 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2017-01-01 to 2026-02-07 14:38:22.049737...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2017-01-01 to 2026-02-07 14:38:22.049737...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-07 14:39:58 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 to 2026-02-07...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 00:00:00 to 2026-02-07 00:00:00...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 00:00:00 to 2026-02-07 00:00:00...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-07 14:39:58 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-01-01 to 2026-02-07...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Using cached date range for 2024-01-01 00:00:00 - 2026-02-07 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:260\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-02-07 00:00:00...\n", + "2026-02-07 14:39:58 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:260\n", + "2026-02-07 14:39:58 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-07 14:39:58 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: Using cached date range for 2024-01-01 00:00:00 - 2026-02-07 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching from source.\n", + "2026-02-07 14:39:58 [test] trade.datamanager.utils INFO: No data requested to load in _load_model_data_timeseries(). Option: Symbol=AAPL, exp=2026-09-18 00:00:00, strike=260.0 right=C Load bools: d=False, r=False, s=False, f=False, opt_spot=False, vol=False, greek=False\n", + "2026-02-07 14:40:00 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-07 14:40:00 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 to 2026-02-07...\n", + "2026-02-07 14:40:00 [test] trade.datamanager.utils INFO: Using cached date range for 2024-01-01 00:00:00 - 2026-02-07 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-07 14:40:00 [test] trade.datamanager.utils INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching from source.\n", + "2026-02-07 14:40:00 [test] trade.datamanager.utils INFO: No data requested to load in _load_model_data_timeseries(). Option: Symbol=AAPL, exp=2026-09-18 00:00:00, strike=260.0 right=C Load bools: d=False, r=False, s=False, f=False, opt_spot=False, vol=False, greek=False\n", + "2026-02-07 14:40:01 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-07 14:40:01 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 to 2026-02-07...\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
RhoVegaDeltaThetaVolgaGammaMidpointCloseaskClosebid
datetime
2025-05-060.6450600.7890200.284534-0.026680-0.0098610.0056709.20010.907.50
2025-05-070.6235030.7822440.279719-0.026940-0.0094690.0055399.10010.257.95
2025-05-080.6536680.7746550.294235-0.027711-0.0085280.00549410.00010.159.85
2025-05-090.6564460.7787280.293327-0.027432-0.0089000.0055809.8259.959.70
2025-05-120.8308700.9189750.352829-0.032195-0.0092290.00594712.82512.9512.70
..............................
2026-02-020.9042950.8001590.650133-0.0637810.0010660.00649830.77530.9030.65
2026-02-030.8896520.7989280.645413-0.0648730.0009180.00640830.87531.1530.60
2026-02-040.9566090.7668800.687810-0.0650340.0015130.00590135.67536.1535.20
2026-02-050.9342470.7670270.680318-0.0670500.0012160.00573236.15036.3535.95
2026-02-060.9642650.7640190.697090-0.0657670.0016330.00579436.75036.9536.55
\n", + "

191 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " Rho Vega Delta Theta Volga Gamma \\\n", + "datetime \n", + "2025-05-06 0.645060 0.789020 0.284534 -0.026680 -0.009861 0.005670 \n", + "2025-05-07 0.623503 0.782244 0.279719 -0.026940 -0.009469 0.005539 \n", + "2025-05-08 0.653668 0.774655 0.294235 -0.027711 -0.008528 0.005494 \n", + "2025-05-09 0.656446 0.778728 0.293327 -0.027432 -0.008900 0.005580 \n", + "2025-05-12 0.830870 0.918975 0.352829 -0.032195 -0.009229 0.005947 \n", + "... ... ... ... ... ... ... \n", + "2026-02-02 0.904295 0.800159 0.650133 -0.063781 0.001066 0.006498 \n", + "2026-02-03 0.889652 0.798928 0.645413 -0.064873 0.000918 0.006408 \n", + "2026-02-04 0.956609 0.766880 0.687810 -0.065034 0.001513 0.005901 \n", + "2026-02-05 0.934247 0.767027 0.680318 -0.067050 0.001216 0.005732 \n", + "2026-02-06 0.964265 0.764019 0.697090 -0.065767 0.001633 0.005794 \n", + "\n", + " Midpoint Closeask Closebid \n", + "datetime \n", + "2025-05-06 9.200 10.90 7.50 \n", + "2025-05-07 9.100 10.25 7.95 \n", + "2025-05-08 10.000 10.15 9.85 \n", + "2025-05-09 9.825 9.95 9.70 \n", + "2025-05-12 12.825 12.95 12.70 \n", + "... ... ... ... \n", + "2026-02-02 30.775 30.90 30.65 \n", + "2026-02-03 30.875 31.15 30.60 \n", + "2026-02-04 35.675 36.15 35.20 \n", + "2026-02-05 36.150 36.35 35.95 \n", + "2026-02-06 36.750 36.95 36.55 \n", + "\n", + "[191 rows x 9 columns]" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "BaseDataManager.CONFIG.option_model = OptionPricingModel.BINOMIAL\n", + "binom_data = new_generate_spot_greeks(\n", + " opttick=\"AAPL20260918C260\",\n", + " start_date=\"2024-01-01\",\n", + " end_date=\"2026-02-07\"\n", + ")\n", + "binom_data" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "420015fd", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+0AAAIoCAYAAAAcFQqgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4VNXWwOHflEwa6Q1IQkJC6L13EJCuAtKUIlIEwcLF3gAVVPSiqKggIChNQBQEBER6771DKCmQXiczmXa+P/IRb0yQEEImhPU+Tx6Tc/bZe80hTmad3VSKoigIIYQQQgghhBCi1FHbOwAhhBBCCCGEEEIUTJJ2IYQQQgghhBCilJKkXQghhBBCCCGEKKUkaRdCCCGEEEIIIUopSdqFEEIIIYQQQohSSpJ2IYQQQgghhBCilJKkXQghhBBCCCGEKKUkaRdCCCGEEEIIIUopSdqFEEIIIYQQQohSSpJ2IYQQopgsWLAAlUrFggUL7B2KKILQ0FBCQ0PtHYYQQgiRhyTtQgghyjSVSpXny9HRET8/Pxo2bMjIkSNZv349Vqv1vsZgj2TwwIEDjBgxgmrVquHm5oajoyMhISH07duX5cuX3/fXLIQQQojiobV3AEIIIURJmDRpEgBWq5XU1FROnz7NwoULmTdvHo0bN2bx4sVUrVrVzlHeO7PZzEsvvcSsWbPQaDS0a9eOHj164OjoSHR0NFu2bGHlypU8+eST/PLLL/YOt1TZvHmzvUMQQggh8pGkXQghxENh8uTJ+Y7FxcXx4osvsmLFCjp16sShQ4fw9/cv+eCK0bhx45gzZw516tRhxYoVVKtWLc95q9XKkiVL+P333+0UYekVHh5u7xCEEEKIfGR4vBBCiIdWQEAAP//8M+3btycqKoqPPvooX5nk5GTeeustatSogbOzMx4eHnTs2JE///zzjvVv27YNlUrFtWvXuHbtWp5h+sOGDcstt2rVKgYPHkzVqlVxdXXF1dWVRo0a8dVXX2Gz2Qr9enbv3s2cOXPw9vZm48aN+RJ2AI1Gw5AhQ1i0aFGe4zabjVmzZtGkSRPKlSuHq6srTZo04bvvviswBpVKRfv27YmLi2P48OEEBATg6upKy5Yt2blzJwB6vZ7XXnuNkJAQHB0dqVWrFitWrMhX1/+uBbBu3TpatmyJq6srXl5e9O3bl4sXL+a75sKFC7z55ps0btwYPz+/3OH/zz33HNHR0fnK3/q3mDx5MgcOHKBHjx54e3ujUqm4evUqUPA0BpPJxFdffUXDhg3x8vLCxcWF0NBQnnjiCf7666987WzevJmuXbvi7e2No6MjVatW5c033yQtLS1f2fbt26NSqbBYLHz00UdERETg6OhIcHAwb7zxBiaTKd81QgghHj7S0y6EEOKhplareffdd9m2bRtLly7liy++QKVSAXDt2jXat2/P1atXadOmDV27dkWv17N27Vq6du3K7NmzGTVq1G3rDg0NZdKkScyYMQOA8ePH556rX79+7vdvvvkmarWaZs2aERgYSFpaGlu2bOHll1/m4MGDLFy4sFCv5fvvvwfgueeeo0KFCv9a1tHRMc/PQ4YMYcmSJQQHBzNy5EhUKhW//fYbY8eOZdeuXSxevDhfHampqbRq1Qo3NzeeeuopkpOT+fnnn+nSpQt79+5l9OjRJCcn07NnT8xmM0uXLmXAgAEEBwfTvHnzfPX9+uuvrF+/nt69e9O+fXuOHTvGypUr2bp1K3v27MnzEOLXX39l1qxZPPLII7Rs2RKdTsfp06eZO3cua9as4dChQwQGBuZrY+/evXz88ce0bt2a4cOHk5iYiE6nu+19GjZsGEuXLqV27doMHToUZ2dnYmNj2bVrFxs2bKBTp065ZWfPns3zzz+Pq6sr/fr1w9/fn23btjFt2jTWrFnD7t278fT0zNfG008/zc6dO+nWrRvu7u788ccffPrpp8THxzN//vzbxiaEEOIhoQghhBBlGKDc6c+d0WhUtFqtAiiRkZG5x9u1a6eoVCpl6dKlecqnpKQo9erVU5ycnJSbN2/mHp8/f74CKPPnz89TPiQkRAkJCblt+5cuXcp3zGq1KkOHDlUAZd++ff8a/y1hYWEKoGzatKlQ5W9ZsmSJAigNGjRQMjIyco9nZmYqjRo1UgBl8eLFea65dV9Hjx6tWK3W3OM//fSTAiheXl5Kz549FYPBkHtux44dCqD06tUrT1237hugrFmzJs+5GTNmKIDSoUOHPMejo6MVo9GY77Vs3LhRUavVypgxY/Ic37p1a24bs2bNKvA+/PPfKTU1VVGpVEqjRo0Ui8WSr3xiYmLu91evXlV0Op3i5uamnD17Nk+5559/XgGUUaNG5Tnerl07BVAaNmyoJCUl5R7PzMxUwsPDFbVardy4caPAWIUQQjw8ZHi8EEKIh56joyM+Pj4AJCQkAHD8+HG2b9/Ok08+ycCBA/OU9/T05P3338doNLJy5cp7br+gudRqtZqXX34ZgI0bNxaqnhs3bgAQFBR0V+3/8MMPAHzyySeUK1cu97irqyvTpk0DYO7cufmuc3Fx4bPPPkOt/vvjxNNPP41WqyUlJYUvv/wSJyen3HNt2rQhNDSUY8eOFRhHhw4d6NmzZ55jL7zwAuHh4WzZsoVr167lHg8MDMw3WgCgc+fO1KpV67b3rH79+owePbrAc/+kUqlQFAVHR8c8r/GWW78zAIsWLcJkMvHCCy9QvXr1POWmTp2Km5sbCxcuJDs7O18906ZNw9vbO/dnV1dXBg0ahM1m49ChQ4WKVQghRNklw+OFEEIIQFEUgNyh8Xv37gUgLS2twEXsbiX3Z8+evee2k5KS+Oyzz/jjjz+IjIxEr9fnOR8TE3PPbfybI0eOoFarad++fb5z7dq1Q6PRcPTo0XznqlatipubW55jGo2GgIAA9Ho9YWFh+a4JDAxk//79BcbRrl27fMc0Gg2tW7fm8uXLHD16lJCQECDn32vx4sUsWLCA48ePk5KSkmcbu9sNeW/atGmBxwvi7u7OY489xpo1a6hfvz5PPvkkbdq0oVmzZri4uOQpe+TIESDnwcM/eXl50aBBA3bs2MG5c+eoV69envONGzfOd01wcDAAKSkphY5XCCFE2SRJuxBCiIee0WgkOTkZAD8/PyAnkQbYtGkTmzZtuu21mZmZ99R2amoqTZo04cqVKzRt2pShQ4fi7e2NVqslNTWVL7/8ssDe2YJUqFCByMhIYmJi8vX2/pu0tDS8vb0LTHS1Wi2+vr7Ex8fnO+fh4VFgfVqt9l/PWSyWAs8FBAQUeLx8+fK5cd4yYcIEZsyYQYUKFejSpQuBgYE4OzsDOQvb/W+vfEF1FdayZcuYNm0aS5Ysyd020MnJib59+/Lf//43N+Zbsd1uLYFbx1NTU/OdK2ieu1ab8xHtfx9ECCGEeDhJ0i6EEOKht2vXLiwWCwEBAbmrh99KOr/88kteeuml+9b23LlzuXLlCpMmTcrXo793716+/PLLQtfVunVrIiMj2bx5Mx07diz0dR4eHiQnJ2M2m3FwcMhzzmKxkJiYiLu7e6HrK6q4uLgCj9+8eTM3ToD4+Hi++uorateuzZ49e/L19i9duvS2bdwaSVFYzs7OTJ48mcmTJxMVFcWOHTtYsGABixYt4urVq7kr5d+K7ebNm9SqVStfPbemLtzuYYYQQghxOzKnXQghxEPNZrMxdepUIGc+9i23Vje/lZTdC41Gc9se00uXLgHw5JNP5ju3ffv2u2rnueeeA3JWkb9dAnzL//beN2jQAJvNxo4dO/KV27FjB1arlYYNG95VLEVR0Ou1Wq3s2rUrN06AyMhIbDYbnTt3zpewR0dHExkZeV/iCw4OZtCgQWzcuJEqVaqwa9eu3BEZt2Lbtm1bvutSU1M5duwYTk5O1KhR477EJoQQouySpF0IIcRDKz4+noEDB7Jt2zYqVarE22+/nXuucePGtGnThl9//TV3obZ/OnnyZIHDxv/Jx8eHhIQEDAZDvnO3evb/mewdPXqUjz/+uPAvBmjVqhWjRo0iKSmJrl27Fri/uc1mY+nSpQwZMiT32PDhwwF46623yMrKyj2elZXFm2++CcCIESPuKpai2LJlC2vXrs1zbObMmVy+fJlHHnkkdz77rXu2a9euPA9DMjMzGTVq1G2H39+thIQETp48me+4Xq8nMzMTrVabO6Vg8ODBODg48PXXX+c+iLnlvffeIz09ncGDBxe4eJ4QQgjxb2R4vBBCiIfCraHnNpuN1NRUTp8+za5duzCZTDRt2pTFixfj6+ub55olS5bQoUMHRowYwVdffUWzZs3w9PQkOjqaEydOcOrUKfbu3Yu/v/+/tt2xY0cOHjxI165dadu2LY6OjtSrV4/HHnuMoUOH8tlnnzF+/Hi2bt1KREQEFy9eZO3atfTp04dly5bd1ev85ptv0Gg0zJo1ixo1atC+fXvq1auHo6MjMTExbNmyhejoaPr27Zt7zdNPP83q1atZvnw5tWrVolevXqhUKlatWsWVK1cYMGAAgwYNuqs4iuKxxx6jd+/e9O7dmypVqnDs2DHWr1+Pt7c33377bW658uXLM3DgQH7++Wfq169P586dSUtLY9OmTTg5OVG/fv3brlB/N2JiYmjQoAF16tShbt26BAcHk56eztq1a7l58yYvvfRSbk9/aGgoM2bMYNy4cTRs2JD+/fvj5+fH9u3b2bt3L9WrV89diV8IIYS4K3beck4IIYS4r/j/vblvfel0OsXHx0dp2LChMnLkSGX9+vV59hn/p/T0dGXq1KlKw4YNFVdXV8XJyUkJDQ1VunfvrsyePVvJzMzMLXu7fdozMzOVMWPGKIGBgYpGo1EA5Zlnnsk9f/r0aeWxxx5T/Pz8FBcXF6Vhw4bKnDlzlCtXruQrW1j79u1Thg8frkRERCiurq6KTqdTgoKClF69einLli3L95qtVqvyzTffKI0aNVKcnZ0VZ2dnpWHDhsrMmTMLvD+A0q5duwLb/rd96W/tTf6//ve+rVmzRmnevLni4uKieHh4KH369FHOnz+frx69Xq+8/fbbSnh4uOLo6KgEBQUpY8eOVRITEwts49Y+7ZMmTbrtPftn3CkpKcr777+vPPLII0rFihUVnU6nlC9fXmnXrp2yZMkSxWaz5atj48aNyqOPPqp4enoqOp1OCQ8PV1577TUlJSWlUPeioHsihBDi4aZSlP/f40YIIYQQwg4WLFjAs88+y/z58xk2bJi9wxFCCCFKFZnTLoQQQgghhBBClFKStAshhBBCCCGEEKWUJO1CCCGEEEIIIUQpJXPahRBCCCGEEEKIUkp62oUQQgghhBBCiFJKknYhhBBCCCGEEKKUkqRdCCGEEEIIIYQopSRpF0IIIYQQQgghSimtvQMoLVJSUrBYLPYOQwghhBBCCCFEGafVavHy8ipc2fscywPDYrFgNpvtHYYQQgghhBBCCJFLhscLIYQQQgghhBCllCTtQgghhBBCCCFEKSVJuxBCCCGEEEIIUUpJ0i6EEEIIIYQQQpRSshBdIWRnZ5OdnW3vMEQZo1KpKFeuHCqVyt6hCCGEEEIIIUopSdrvQK/Xo1KpcHNzk+RKFCuTyURmZiZubm72DkUIIYQQQghRSsnw+DuwWCy4uLhIwi6KnU6nQ1EUe4chhBBCCCGEKMUkab8DSdaFEEIIIYQQQtiLJO1CCCGEEEIIIUQpJUm7EEIIIYQQQghRSknSLmjWrBlz5sz51zKBgYFs2LChROKZPn06jz76aIm0JYQQQgghhBClmSTtZVhMTAwTJkygYcOGhIaG0rRpUyZOnEhycnKxtzV+/HgCAwMJDAwkJCSEevXqMXDgQH7++WdsNts91z18+PBiilQIIYQQQgghHhyStJdR165do3v37ly5coVvvvmG3bt388knn7Br1y4ef/xxUlJSir3NRx55hKNHj7Jv3z4WLVpEy5YtmThxIs888wwWi6XY2xNCCCGEEEKIsk72ab9LigIGg31WlHd2VijsYvbvvPMODg4OLFmyBGdnZyBniHvt2rVp2bIl06ZN45NPPinw2sjISF599VWOHTtGpUqV+OCDDwrVpk6nw9/fH4AKFSpQp04dGjZsyIABA1i+fDlPP/00AGlpaXz44Yds3LgRk8lE3bp1mTx5MrVq1cpX5/Tp01mxYkVu/AArVqygZcuWTJ06lfXr13Pjxg38/f3p3bs3//nPf3BwcCjcTRJCCCGEEEKIUk6S9rtkMKiIiKhgl7YvXryBi8ud9/VOSUlh27ZtvPHGG7kJ+y3+/v706dOHNWvW8PHHH+fb0s5mszFq1Ch8fX1Zs2YNGRkZTJo0qcgxt27dmpo1a7J+/frcpH306NE4OTmxaNEi3NzcWLRoEQMGDGDnzp14eXnluX7MmDFcvHiRzMxMPv/8cwA8PT0BcHV15YsvvqB8+fKcPXuW119/nXLlyjF27NgixyuEEEIIIYQQpYkk7WXQlStXUBSFiIiIAs9XqVKF1NRUkpKS8PX1zXNu586dXLp0icWLF1O+fHkA3nzzTQYPHlzkeKpUqcLZs2cBOHDgAMeOHeP48eM4OjoCMHHiRDZu3Mi6devytePq6oqTkxMmkym3F/+W8ePH534fHBxMZGQkq1evlqRdCCGEEEIIUWZI0n6XnJ0VLl68Ybe274ai3F15gIsXL1KxYsXchB2gUaNGd13PP+O41aN/5swZ9Ho9tWvXzlPGaDRy7dq1u6p39erV/PDDD1y7dg29Xo/VaqVcuXL3FKsQQgghRGl1IeUCPk4++Dj72DsUIUQJkqT9LqlUFGqIuj2FhoaiUqm4ePEi3bp1y3f+0qVLeHp64uNTMm/4ly5dIjg4GAC9Xo+/vz+//PJLvnIeHh6FrvPQoUO8+OKLvPLKK7Rv3x43NzdWr17N999/X2xxCyGEEEKUFhdTLvLoykeJ8IpgU59N+aY4CiHKLknayyBvb2/atm3Ljz/+yKhRo/LMa4+Pj+fXX3+lb9++Bb7ZR0REEBsbS1xcHAEBAQAcOXKkyLHs2rWLs2fPMmrUKADq1KlDQkICWq02N5G/E51Oh9VqzXPs0KFDBAUF8fLLL+cei4mJKXKcQgghhBCl2ZH4I1gUCzFpl9FmnMDqXs/eIQkhSohs+VZGTZkyBZPJxKBBg9i3bx8xMTFs3bqVp556ivLly/PGG28UeF2bNm0ICwtj/PjxnD59mv379zNt2rRCtWkymYiPj+fGjRucPHmSr776iuHDh9OpUyf69u2bW3+jRo0YPnw427dvJyoqioMHD/LJJ59w/PjxAusNCgri7NmzXLp0ieTkZMxmM2FhYcTExLB69WquXr3KvHnzWL9+fdFulhBCCCFEKTcwvCfxLZ7gZLCJcjcW2zscIUQJkqS9jAoLC2P9+vVUqlSJMWPG0KpVK15//XVatmzJ77//nm+V9lvUajVz587FaDTSs2dPXn311dsm+P+0detWGjRoQPPmzRk0aBB79uzhww8/ZP78+Wg0GgBUKhULFy6kefPmTJgwgTZt2jB27FhiYmLyLYp3y6BBgwgPD6d79+7UqVOHgwcP0rlzZ0aNGsU777xD586dOXToUJ6F6YQQQgghygqVOY3yu+vgl7iaSg7gHL8alTXL3mEJIUqISinKamVlUEJCAmazOd/x9PR03N3d7RCReBjI75cQQggh7sQp/ne8zzyP2TmcJHM2yxKi8a08gkfqf2Dv0IQQReTg4ICfn1+hysqcdiGEEEIIIUqx1/Z/Rlw6NElozBX3RH60RtPcsFySdiEeEpK0CyGEEEIIUUpZrWZ+T7hCohVi9rcg7kormo5twmCXDNT6SGyuYfYOUQhxn8mcdiGEEEIIIUopx8xTbA5U+NBLx6k9o7gR1ZgfPTsxzhNc41bYOzwhRAmQpF0IIYQQQohSyillK3Udoc71nlgsLgB8uWYEVq0Xaku6naMTQpQESdqFEEIIIYQopZySNgOw+lAPvLysuLvb+H59Pxabj7K+XA8SshLsHKEQ4n6TpF0IIYQQQohSKF4fx+tpXsy/2YL1x7vxyCPZ9O5twKZoeOXoMPqt68fGaxvtHaYQ4j6TpF0IIYQQQohSaNbJ2Xx5eStjr1i5mVqBTp2MPPVUzv7sycfa4OcUgNmcat8ghRD3nSTtQgghhBBClDaKwlPVnqKFXweMGyajViu0a5dNnTpmatUyU+1CHy6WV/OG/id7RyqEuM8kaRdCCCGEEKKU8Tz3Eg1TV9M77Se41I3GjU14eioAPPWUnmvxEbioE9Bmx6AxRNk5WiHE/SRJu3igNGvWjDlz5tg7DCGEEEKI+8YheTcucb9S7tqXnD98E4COHbNzzz/xhAGDyYWDl5sAoE3dbZc4hRAlQ5L2Mmr8+PEEBgYyc+bMPMc3bNhAYGBgicYSGBiY+1WlShVatWrF+PHjOXHiRLHUvWHDhmKIUgghhBDC/qxWE702PMPHyRDv+zSL1zUAoGNHY24Zb2+FGjUszI4OpP41mHBw5u2qE0KUAZK0l2FOTk58++23pKam2jsUPv/8c44ePcrWrVv56KOP0Ov19OzZkxUrVtg7NCGEEEKIUmPzsffYozfwWYqKn069hNGopkIFK9WrW/KUa9Eim9PX63LcBLtSZHi8EGWZJO1FpLJm3fYLq/EuyhoKVbYoWrdujZ+fX77e9n86cOAAvXv3Jjw8nMaNG/Pee++RlZXT5vz58+nQoUNu2Vs99T/99PeiJwMGDGDatGn/2oaHhwf+/v4EBwfTrl075syZQ+/evXn33XfzPFT4t1j+qVmzZgCMGDGCwMDA3J+vXr3Ks88+S7169YiIiKB79+7s2LHjX+MTQgghhLA3lSWdQYY/WBQA71frxX+nNQRg5MhMVKq8ZVu0MHH+wHCWBqjYHWhBbYy1Q8RCiJIgSXsRVdgZcdsv79Oj8pQN2F33tmV9TgzJU9Z/X7MCyxWFRqPhzTffZP78+cTGFvxGfvXqVQYNGkT37t3ZtGkT3333HQcOHOCdd94BoHnz5ly4cIGkpCQA9u7di7e3N3v37gXAbDZz+PBhWrRocdfxjRo1iszMzNyE+k6x/NMff/wB/N2Lf+tnvV5Phw4dWLZsGRs3bqR9+/Y8++yzxMTE3HWMQgghhBAlxe3aVzhYkukfEM6lXXOJj9cQGmph+HB9vrLNmpnITAskPKER5bXgmLbPDhELIUqCJO1lXLdu3ahZsybTp08v8PzMmTPp3bs3o0aNIiwsjCZNmvDhhx/yyy+/YDQaqV69Op6enrlJ+t69exk9ejT79uX8YTh27BgWi4UmTZrcdWxVqlQBIDo6ulCx/JOPjw/wdy/+rZ9r1arFkCFDqF69OmFhYbz++uuEhITw559/3nWMQgghhBAlIV0fg2PMQgAuub/PrNleAEyalIZOl7+8t7eN6tXNLNw9hJOmMZhdqpZkuEKIEqS1dwAPqhttLt72nPKPZyFxrW6/4JpC3rFO8c3331tgBXjnnXfo378/Y8aMyXfuzJkznD17lt9+++3vmBQFm81GVFQUERERNG/enL1799KmTRsuXrzIM888w3fffcelS5fYu3cv9erVw9nZ+a7jUpScbUtU/z/eqzCxFIZer2f69Ols3ryZ+Ph4LBYLRqNRetqFEEIIUWq9smcyV1PL8021Vnw6vTcmk4q2bY08+mj2ba9p0SKbrxcP52jEYup5/s7bTWuXYMRCiJJSKpP2DRs2sGbNGlJTUwkJCWH48OG5vbL/NHnyZM6cOZPveIMGDXjrrbfuW4yKxsXuZQurefPmtGvXjo8//pj+/fvnOafX6xk8eDDDhw/Pd92tVeZbtGjB4sWL2b9/P7Vq1cLNzY1mzZqxZ88e9u3bR/PmzYsU16VLlwAIDg4udCyF8cEHH7Bz507ee+89QkNDcXJy4rnnnsNkMhUpTiGEEEKI+yk+K549N/aQbkrnUPwPrF/vgkajMHlyer657P+reXMT85eq2OX1IruOm+kc0pnGAY1LLnAhRIkodUn7nj17+Omnnxg1ahQRERGsW7eOqVOnMmPGDDw8PPKVf/XVV7FY/l5NMyMjg9dee61Ic6zLsrfffpvOnTsTHh6e53idOnW4cOEClStXvu21zZs3Z9KkSaxdu5aWLVsCOYn8zp07OXjwIKNHjy5STHPmzMHNzY02bdoUOpZ/cnBwwGq15jl26NAh+vXrR7du3YCchwG3huALIYQQQpQqipVA60129t/JjqhdzHw+pzNk6FA91apZ/vXS5s1NYCwP+1/ghcHXqZ22ASRpF6LMKXVz2teuXUvHjh155JFHCAoKYtSoUeh0OrZu3Vpg+XLlyuHp6Zn7deLECRwdHYvc+1tW1ahRg969e/PDDz/kOT527FgOHTrEO++8w6lTp4iMjGTjxo15Fn+rWbMmHh4erFq1KvdhSIsWLdi4cSMmk6lQ89nT0tKIj48nOjqaHTt2MGrUKFatWsXHH3+c+zCmMLH8U1BQELt27SI+Pj53FfrKlSuzfv16Tp06xenTpxk3bhw2m+1ub5kQQgghxH3ncmMpfoe7ERr1Oel7B3L2rAOenjYmTMi447W+vjYiIsyEHn2Rr9S/Ehb7HdrMcyUQtRCiJJWqpN1isRAZGUmdOnVyj6nV6twe2MLYsmULLVu2xMnJqcDzZrOZrKys3C+DwVBgubLotddey5e81qxZk5UrVxIZGUmfPn3o0qULn332GQEBAbllVCoVzZo1Q6VS0bRp09zr3NzcqFu3Li4udx7SP2HCBBo0aEC7du146623cHV1Zd26dfTu3fuuYvmniRMnsmPHDpo0aUKXLl0AmDRpEh4eHjzxxBMMGzaM9u3b5/mdEkIIIYQoDfSGG1w68xEAmVTm00/dAHj11XS8vZVC1dGihYmrCZU5nPAEAOWivr0/wQoh7Eal3FoNrBRITk5mzJgxTJkyhapV/14Bc9GiRZw5c4aPPvroX6+/dOkSb7/9Nh999NFt58AvX76cX375JffnypUrM23aNBISEjCbzfnKp6en4+7uXsRXJMS/k98vIYQQ4uH1+cYefH79GO8EeJFw5DKz53hRtaqZTZsS0BZyEuvq1U6MHetNv477mDioBf9NUfHyo6sJ8Gl0f4MXQtwTBwcH/Pz8ClW21M1pvxdbtmyhUqVKt03YAXr37k3Pnj1zf1b92+oeQgghhBBC3AeajLMkpx5HAfw8hvPJfE8A3n8/vdAJO+T0tKtUCis2N+dGFw92ZaXhduAt3upWhK1ubdlosm9gdQ69+2uFEPdNqRoe7+7ujlqtzp2bfEtqaiqenp7/eq3RaGT37t106NDhX8s5ODjg4uKS+1WUrcqEEEIIIYQoKl3KbvyO9+WHAIWDdZqyau6HWCwqHn3USNu2t9/irSD+/ja6dTMCUDlmAn3LwTDVabxOjUBtjL2rutzPvUbA/la4Rs2+q+uEKC1UlnTcL3+IU8I6KD0Dyu9ZqUratVotYWFhnDp1KveYzWbj1KlTeYbLF2Tfvn1YLJbclciFEEIIIYQobbZdXY/TqdGoLamY3BpwRf8jW7c44+CgMHFiWpHqHDs2E4Cl37zLN9WH0sgJHFP3YlI788r2VzgUd+iOdSQkH6f2/pW8nwTlLn2AVn++SLEIYU8O6ccoFzUL98tT+Nf9Eh8wpSppB+jZsyebN29m27ZtREdHM3fuXLKzs2nfvj0AM2fOZMmSJfmu27JlC02aNMHNza2EIxZCCCGEEOLO5pycw6BNIxmcUZVM/97cqLmCt97P2Y53xAg9YWHWO9RQsAYNzLRqlY3Foubt374hvsk2UqtN57erf/HzhZ8ZuWkkLqdfxCl+DSgFt7Hq2Idcs8CfWZBR7TP2ZqZzOuk02LLRpR3E+eYvYDUW+bULURKib24myQom9wb2DqVYlbo57S1btiQ9PZ3ly5eTmppKaGgob7/9du7w+MTExHzz0GNjYzl37hzvvvuuHSIWQgghhBDi9lSWTLSGK9TxrYOD2gF/r7qkVH+P+XPduXzZAV9fKy+/fOct3v7NCy9ksnu3I0uWuDB+fFW8/SJopItkYNWB1HTU4JmwGBJ+xeJcmbXOnWhU83U02v/fAchq5HXdOaqVB13YeDYqwTyzbgAt/Ovxp9txHJRsFAWi43fhU3fGvd8QIe6Tt07/xoZUmOmqovcdSz84StXq8fYkq8cLe5DfLyGEEKJsUxtj8Dn5DJrsGyQ0/J0LJgj3DCcpSU3r1v6kp6v57LNUnn46657aURTo2tWXU6d0vPJKOhMmZP4dgykZ15gfcI2Zz6HMVJpFQTWdlhXdF+Lj1xbnmyvwOjcei2NFrtTZh8Uhkyd+f4LgckFsKneYVBv0jkrngBH2992Ar6dspStKIUWh7+JQ9hosdD4xmw/feIKgoKKNXikJd7N6fKkbHi+EEEIIIURZkJp2hpd/b09y2lkUtQ61VU+4Z85w+E8+cSM9XU3t2iYGDLi3hB1ypu+OG5eTqH/7bTmmTnXjxo2cj/o2nTcZlV8lrvkBzvn0xVujop2ThZrnRuOQfgSnpM0A/H52FDVrBfHX2vKs6LGCXlV6E990C4Y2pzGpy2EDTp6Zds+xCnE/aIzX2BNkITbYgd0bn8bPr/Qm7HdLknYhhBBCCCGKmTo7jtfXP87ytCwGxjuR0HAtZre6ABw54sDSpTlD0z/8MB2Npnja7NHDSIsW2RgMar791o0WLQIYP96Ts2dzZsQqWlc6NfmSXf338GWVRmis6Vw9+hJtL8XyjfNLvDhjHBaLilde8eT8kUCejHgSm2MFVGo1n7acxOUQeNq8+65XpReiJOjSjwJwLboh1avrcHS0c0DFSJJ2IYQQQgghipHalIDP8f7M8DbQyNmBDzr+iM0pEACrFd55xwNFUdGvXxZNm5qKrV2NBpYvT2L+/CSaN8/GbFaxYoULnTr5M2iQNzt26FAU8ChXiZR6S8kK6MOJ8iM5nXyGmUdiiE3yQ6NRMJtVjBzpzdmzWqxWOHVKy9lTz+Lr0xyVYsL1Rv5FoYWwN5uDF8fiu7LxRBcaNSq+/69KA0naH2JRUVEEBgbm2WLPXqZPn86jjz56V9cEBgayYcOG+xRRXkWJTwghhBAPIZsFr1OjcMi6RGW3Cqzts53wgNa5pxcvduHECR3u7jbeeSe92JtXq6Fz52xWrkxizZoEevY0oFYrbNvmxFNP+dKlix+//uqMSXEltcbXpKucsdgsxP/2OgCffppK8+bZZGSo6dvXlzp1ytOliz+jR/vw5fYppNT4mvN+A0kyJBV77EIUljr7JqbLM/hyxxiiMqIAGHZkKe1PeDN562BJ2sWDYfz48QQGBuZ+1apVi0GDBnHmzJncMhUrVuTo0aNUr17djpHmGDNmDMuWLSvWOv/3HoSEhFCvXj0GDhzIzz//jM1mu+e6hw8fXkyRCiGEEKKsWH7uJ04bsrBp3Eist4xMQlm92omff3ZmyRIXpk3LWYD29dfT8fO7t88jd9KwoZnZs1PYtSueZ5/NxNnZxunTDrz4ohdt2vhz5oyW/hH9+cjzIsmnmxIQYKVPHwNz5yZTpYqZ1FQ1aWlqXFxy4pw8swMfXU6l1bI2fHXsq/sauxD/xkF/gZf3f8an59fw7fFvMVqMbLy2kbSQJWDTStIuHhyPPPIIR48e5ejRoyxbtgyNRsMzzzyTe16j0eDv749Wa/+d/1xdXfH29i72em/dg3379rFo0SJatmzJxIkTeeaZZ7BYLMXenhBCCCEeXkfij/Danvdpdf4S+8JnkJhdhT59fBg71ptXXvHitdc8SU1VU7OmmSFD7n3xucIKCbEyZUo6Bw7E8frr6fj6WomK0tKvny9Hj+pYMr8CAEOG6NHpwMtL4Zdfkvj001T++COBs2dv0qRJNkajipNb62CymbiccgFMqSX2GoT4X2pzIhM8oa6zK+3KN8TBcJW3K34Nf31MBZcgKlS4vw/ESpok7UWUZc4iy5zF/+6YZ7KayDJnkW3NLrCsTfn7l8dsM5NlzsJoMRaqbFHodDr8/f3x9/endu3avPDCC8TGxpKUlDOc6Z/D4/fs2UNgYCA7d+6kW7duhIeH8/jjj3Pp0qU89f7444+0bNmS0NBQ2rRpwy+//JLnfGBgIAsXLmTo0KGEh4fTrl07Dh06xJUrV+jbty9VqlTh8ccf5+rVq7nX/HP4+bFjxxg4cCC1a9emevXqPPnkk5w8ebLI96BChQrUqVOHl156iR9++IEtW7awfPny3HJpaWm8+uqr1KlTh2rVqtGvXz9Onz5dYJ3Tp09nxYoVbNy4Mbcnf8+ePQBMnTqV1q1bEx4eTosWLfj0008L3EpQCCGEEGWMzUIlt0q0DmxNl9CuuDt15amnfDh5UoeXl5WOHY107Gike3cDX3+dgj36TLy9FV5+OZPt2+Np2NBEaqo6N3HX6RQGD/77QYKfn41Bg7KoV8+MVguvvpqzj/yWuY+xosFENnlewOPqpyX/IoQA1kft4scMeMNdz7Ab71LxRD8mO49hfq1zNG5U9jrmJGkvoogFEUQsiCDZmJx77LsT3xGxIIJ3d7+bp2zdRXWJWBBBTGZM7rEFpxcQsSCCV3e8mqdss5+bEbEggospF3OPLb+wnHul1+tZuXIloaGheHl5/WvZadOmMXHiRNavX49Wq+WVV17JPbd+/XomTZrEc889x+bNmxk8eDATJkxg9+7deeqYMWMGffv25c8//6RKlSq88MILvPHGG7zwwgusX78eRVF49913/9l0rszMTPr168eqVatYs2YNlStXZsiQIWRmZt72msJq3bo1NWvWZP369bnHRo8eTWJiIosWLWL9+vXUqVOHAQMGkJKSku/6MWPG8Nhjj+UZydC4cWMgZ8TAF198wbZt23j//fdZsmQJc+bMueeYhRBCCFG6eZ57mbCYr1nYeR7v1f2cp5/25eRJHT4+Vn75JYmffkrmp5+SmTMnherV7ZtUeHoq/PxzEq1b5/SeAzzxhOFfh+u3amWiRYtszCY1F7c0R2O6icuNpaiNMbe9Roj75WDCJeanw8ZUL9TWTDTmnJzswo2qZW5oPEjSXqb99ddfREREEBERQdWqVdm0aROzZs1Crf73f/Y33niDFi1aULVqVcaNG8ehQ4cwGnNGBMyaNYv+/fszbNgwwsPDGT16NN26dWPWrFl56hgwYACPP/444eHhjB07lqioKPr06UP79u2JiIhg5MiR7N2797YxtG7dmieffJIqVaoQERHBp59+isFg+Ndr7kaVKlWIispZtOLAgQMcO3aM2bNnU69ePcLCwpg4cSIeHh6sW7cu37Wurq44OTnlGcmg0+mAnLnuTZo0ITg4mM6dOzNmzBjWrFlTLDELIYQQonTSGK7iHP875aLnotVf4eWxgbkJ+/LlSXZP0gvi6qrw449JPP64AQ8PG2PH/nvHiEoFr7yS09s+6duupDu3xGQ1cerkxJIIV4g8Wmq8+MgHzGceJ8bckK4xMDwO9lxuUiaTdvtPZn5AXRyW0xPurHXOPfZ83ecZVXsUGnXezTZPDD4BgJPWKffYsFrDGFR9EGpV3gR6/8D9+cr2r9q/SDG2bNmSjz/+GMgZ/v3jjz8yePBg1q1bR1BQ0G2vq1mzZu73AQEBACQlJREYGMilS5cYNGhQnvJNmjRh3rx5eY7VqFEj93s/Pz+APAve+fr6YjQaycjIwM3NLV8MCQkJfPrpp+zZs4ekpCSsVisGg4GYmOJ5mqsoCipVzpPlM2fOoNfrqV27dp4yRqORa9eu3VW9q1ev5ocffuDatWvo9XqsVivlypUrlpiFEEKIh5lj8jY8z75MarXPyPbtbO9w8vh21wvY0m2MCm3N1/OasHu3Iy4uNpYtK50J+y1OTvDddynYbDmrzt9JixYmWrfOZtcuR6ZuG8dS/z0kWDewL/w4ft717n/AQvy/ZhoN/bxhVHQrnl1fiU01jgDgHV+d2rXL3tRUSdqLyMXBJd8xnUaHTqMrVFkHtQMOaodCly1SjC4uVK5cOffnOnXqUL16dRYvXswbb7xx2+sKWpjubldbd3D4O+ZbyfH/1nvr2O3qHT9+PCkpKXzwwQcEBQWh0+l4/PHHi21++KVLlwgODgZypg74+/vnm5sP4OHhUeg6Dx06xIsvvsgrr7xC+/btcXNzY/Xq1Xz//ffFErMQQgjxMHO/9AEacyI+p54ltl10TtdvKZCZeY0vrh8lwwZuru35/POczoiPP06jRo3Sm7D/r8Ik7Le8+moGu3Y5Mv3bgTT6aAzW7AziLs3Ar+n8+xegEP+gtSaCBuLT/dm0dQQ9XttFcnQQllB/dLqytx2hJO0PEZVKhVqtzh3qXhRVqlTh0KFD9O//d+//wYMHiYiIKI4Q89T50Ucf0bFjRwBiYmJITk6+w1WFs2vXLs6ePcuoUaOAnIcZCQkJaLXa3ET+TnQ6HVarNc+xQ4cOERQUxMsvv5x7rLhGBgghhBAPuxEZlYm7eZ6J3hCSehC1V1N7hwSAX8Iv/OAPvxo9+fydidhsKvr1y6JvX4O9Q7svmjQx0b69kW3bnGhx7Rs+aT4UR8NW4o0xWJ0C7R2eeEjMvPI6B9dbuRDdGFCz7rMtAIwZc+/rX5VGMqe9DDOZTMTHxxMfH8/Fixd599130ev1eVZpv1vPP/88y5cv58cffyQyMpLZs2ezfv16xowZU4yRQ+XKlVm5ciUXL17kyJEjvPjiizg5Od35wn+4dQ9u3LjByZMn+eqrrxg+fDidOnWib9++ALRp04ZGjRoxfPhwtm/fTlRUFAcPHuSTTz7h+PHjBdYbFBTE2bNnuXTpEsnJyZjNZsLCwoiJiWH16tVcvXqVefPm5VnsTgghhBBFtyfpIn9mQetomLL7dXuHk8NqwD32R/q6QaPLM4i76UCVKmamTk2zd2T31a257TNnDcbo3BqVYsYx8c/c82pjDF6nRuGUkH9tICGKw2cM568eT9PsiST8/P7uSCuL89lBkvYybevWrTRo0IAGDRrQs2dPjh8/zuzZs2nZsmWR6+zatSvvv/8+s2fPpkOHDixatIjPP//8nuosyPTp00lLS6Nr16689NJLDB8+HF9f37uu59Y9aN68OYMGDWLPnj18+OGHzJ8/H40mZ+0BlUrFwoULad68ORMmTKBNmzaMHTuWmJiY27Y5aNAgwsPD6d69O3Xq1OHgwYN07tyZUaNG8c4779C5c2cOHTrE+PHj7+U2CCGEEOL/fdbmM3oHtwbgt/iLWE35d3gpaS5xv6AxJ5GtCeSt754G4OuvU3F1Ve5w5YOtYUMzHTsasVpVfPjHZyQ0XMdWTU3is+Ixm1J55/fWxMT+gceFt+5bDI6Jf+J25VNsii3PFsyFoc66hi51L9iy71xYlDpGixGVLWdKcnhFL8aN+7t3vawm7Srlbn/Ly6iEhIQC50unp6fj7u5uh4jEw0B+v4QQQog7U5lTST20lHnLqxPY/j8MKZeAtsYnZFUcYreYFEXhxb+G09PFhPXY4zz30X/o0MHIwoXFM52vtDtxwoFu3fxQqxX6zhnL8qhZjKw9AvfkrXweG0llLZwPhcQ2Z1G0xftZR2XRE7C3MQb/nqxx6cqru95icPVBvBZck2zvtqB2/P9ymWj1Z3HIPI0hoB8v73qHHTE7WFKtMR0y12Fyb0hSnZ9QHP59O2RRuqjMyfzy6Tp2Hg2k3aDudO1qZPhwb8qXt/HFF6n2Dq/QHBwcchfsvhOZ0y6EEEIIIUo1bdZlamRP4cVWlVh59DV827+OPuOkXWPaFr2N367+yUatK9ZvfgXg+efL5nzagtSta6ZLFwMbNzoTvbU7DlXnoUveyauOkexxUvFqjSdJqDuZbJUjjsXctjp2KWprOk5Jm/kjXk1MZgw3k4/hk/YpNo0bB7TVmBB5Eiclmy3/v2GSuVwtEg2JxGXFcdTiTAdAl36Em3sfw7n+fNzdi3d9JnH/aA3XeLHVmzxerRL7fLvi7AxLl5bth2UyPF4IIYQQQpRq15OOskEP+xID+HDxSPb57CCt2qd2jameXz1eb/w69TImkJ3mRYMGJlq0KJtDc29nwoScue17Fvdgc61X+Mb1AhW0sLbTfzmuq0GdZW359vi3BV5rtpl5ceuLTNk/5a6Gtys2K912TOWJWDjlO4hJLSYzt9NcRoQ0x6orj9qagU/GIfZmZbPXCNkOARi9O4JKw38a/ofVj6/miUYfE994M6cVHzpfukL/1Y+SnHysOG6JKAFqUyIA8Wn++Pre3Q5XDyrpaRdCCCGEEKXaH1HbmRgLLUwJpGZ58fFKB1QJYzFbzczuNBu1qmT7oRzSDlHl+kxeqDCO72c/BuT0speSXehKTO3aFrp3N/DHH86sXPUYLR9fgL7iYM5nD2LpwsUkV03mWMKxAq/ddG0Tv17KGaHwcmAEbkEDCtXm5as/cdRg4pwKPg4ciLPWmW6VuwEQFz4KXfphfDJOMss/juoVO5Lg1zj396PR/4zStzhUJ6HaDDRXn8EFM/Fn38O71Zqi3wxRYrZF72HtTfDIyuZZSdqFEEIIIYSwPw+bnro6cIkLBeCvLRrMwevRqlREnXyLkDqflOi+7eWiZuOUtInEyABSU5+gcmULXbsWfUvdB9krr2Swfr0TXy5sSc+hxynvbmXw4z7cTHuc17yP8k6TKPSKFVSaPNd1LV+PwdWeorF+O9UuTSDZyROjb5c7ttc0YxNnQ2Bddifef7cWtzrpvb1tPPdcJuXLNwGPJjwWVPD1Fgtcvqzl+nUNrVt3ZGfvtVQ51gOV+QhxWZewulS511si7rMj8RdZkAGtbFlM8JKkXQghhBBCCLsb5ubKSyEw8s9BVKpk4frlJozQfcHo8q/TJHkRadHh6IOfK5FYYuJ38+rJP3jVCyZ8/xqQsze0RnOHC8uo6tUtPPaYkd9/d+aTT9zJyFBx5YoWjTqUiQ1+o1yqnmz9BSzlauReo9Vfwu9ge+Z4P4KlQheImY/n2ZeJb7gWm+vtk2at/gJOKdupplMzcOp3HL/kkuf8zz+7MGlSGv37G1CpQK9XcfasllOnHDhzxoHTpx04d84BozHnAU+DBiYWLqxPtk8ntPqLaLLjJGl/ADR18OAjH4iMq4P2IclmZU57IdhsD8cTHFGyZOMGIYQQonBUWdEAZNiCGTpUD8Dhn0dStforALhf/oCMa/OgBP62Ljg0kaWZ8FKCN9uP1cXPz0rfvln3vd3SbMKEDFQqhc2bnThwwBE3Nxt+/ioOXG4KgC79cJ7yLjeWoEIBlYb08ElkezRjbVoGg37vRrbx9guKaZO3A3DR0J3jl6pQsaKFd99N45130qlXz0RampoJE7zo0cOXNm38qVatPE884cc773iyeLErx47pMBpVuLracHW1cfSojief9OW89wximmxhRXIierP+/t0oUSzqqbW85Q1VslrYO5QSI0n7Hbi4uJCRkSGJuyh2WVlZODoW93qqQgghRBmjKDhZc5J2i2Mw/fsb0OkUjh3TsTd5HPoKg4izKLT8ayKvrWpKVmbkfQtFbUpilDaSwW7gfPgNAEaM0OPkdN+afCBERFjo3dsAgFarMGdOMh06GFlzrSofJsG8sz/nljVbMhlw8AfmpsErc0bwzLMBRIZOZ0S8ii2ZWSze3q/Ahy96s556u+YyVjOAFxa8C8CoUXqef17P2LGZ/P57Im+/nY6jo8Lx4zoiI7UoioqAACsdOhh58cUMZs1KZteuOM6du8maNYmUL2/l/HkHHu9blT6/DWbslrGsuLiiZG6aKDLN/y9El63ytXMkJechGVBQdFqtFldXVzIzH54tPMT9pygKWq1WknYhhBDiDlKzU2lwyRclzpd27gH4+GTTvbuBVatc+OBDDxYv+oi/kpNJsa3nbEYswUd7klnrO7K92xV7LK4xC6ivM/FFQAP8VryGq6stt+f/YffWW+lkZqro189AmzYmYmM1LN7ky+FkqKk/xVP/X27H6en8kWnmoEFN0urHsFmcmPZZfb59ZhI7T7zP67pzGKJnow8ek6f+dVfWEZ0ZzW+XD3JzdxNcXW0MHPj3CAetFsaNy6RbNwM7dzoSGmqlZk0zfn4Fd7xVq2bht98SGTjQh2vXtDgeeBK/uhdwzjgJBczBF6XHgtgX2Pbbc1QKa2jvUEqMJO2FoNVqcXd3v3NBIYQQQghRrKIzY4jnOrgaqRioA7J5+eVM/vrLib17HXl5vB/ffjuXtRV+ofz1L3G0RsL1mcWetKusBlxiFwAwY8MrgIrBg/V4eMh0N4CKFW3Mn5+S+3OjRiY+/GQQT3WcSitnM5iSQefNI6bjfOoLB690YIUlZ076jz+6Uq/eS3zQ2gHdxXfQXP+OrAqDULRuOZUpCv0qtcWr8wL++7kzNxU1Awdm4u6e/96HhVkJCyvcdIVKlawsWpRE27b+XFz2PEk9Z+Ft+JnkxI4Y/brf+00R98U044voe9zg8Rs7ATd7h1MiZHi8EEIIIYQotSp7VKb+ifWwZg7BwVYAqla1MGdOMg4OCmvWODN5sjv1QvpSoeUGkmovIKnez3eo9e6tilzLO6b6nFWa88mSATg4KIwcKSMxbyc83IpiqsZkIhjnCY4ZR3G9Posg/X5e9VJx4Mc56HQKI0bk3MO33vJkf9JI0kNfJaHBGhZfWkNCVgIAutQ9VNzfjM5xGzn1S39UKoXhw4tnhENYmJVu3YxgcWbnhScA8Dz7EqdOvIdisxRLG6L4KIqC1ZbzPlDRw8fO0ZQcSdqFEEIIIUR+VgMo9l/TxyvzCE+5H6W51Y+goL+TqLZtTcyYkQrAvHnlmDTJnSyzK1Eu9fjo4DRe3vZyscVgU2xMP/o10y5toc/mJ7DatPTubaBiRfvfn9JKpYLGjU3svdiCyGwPxuz/koS0cwBsvjaca4mh9OhhYPLkdDp1MpKdrWLkKG+uu7/CtDPLeG3nazy/5XksNguusT+hUiycOZfTM9+li5HQUGuxxfrcczkPAJ774m0yXVuyM9NAl/0/MHR5bZT0k8XWjrh3amsWX2aOZ9DmeYR6B9g7nBIjSbsQQgghhMhDm3mW8rtr43VmnL1DwSlhI68/+jaPN/o9t6f9ll69DEyalAbkJO6dO/uz90g23xz/hpUXV3It6USxxKAoCq80eoUGnq04t+R5AJ5/XnrZ76RxYxPjFnxDv63fsCH+PC/EphBbeSZPTJkNwODBWajV8NVXKYSGWoiO1jJ2rBePV+6Np6MnXX1DsJ7+D+2PrGV2GrwxbywAI0cW7zoCjRubaNDARHyqF+/vXMdpn/44qaCKKoOKR3viduUzkF73UkFtusFzTd5m5pAJ+PvbO5qSI0m7EEIIIYTIw3bla9Q2I2D/nuQ/o4+zQQ8XUr2pWDF/7+pzz+mZPz+JgAArkZFaxg1sSj/HnvwaXI5aiUuLJQbvC28w0MVAnUO/Q7YHjz5qpGpVSeLupHFjE3pdOkeDn6eye2Veb/IWP24bRFaWhogIM82amQDw8FCYNy8ZZ2cbO3c68uv3Ddj/xDLeM65g+cVfOWCE71JcOXypITVqmGne3FSscapUMGpUzkOY+Qvc6FrvC7b2WsXkah1RKRYc0o+QasrgQsqFYm1X3D2NKQmA+HR/fHyKb7RFaSdJuxBCCCGEyGUw3KTGgdV0j4ET7t34+ODHGC1Gu8UzOfoM3WIh0jWT22260rlzNlu2xPPkk1koigrjby/RyymDcvErUVky7i0AxYpL3C94nX+FQ9tztpoaN0562Qujfn0zmqwKKJ/GMbfpn1Tzqs6iRa4ADBqUhUr1d9nq1S1Mn54KwMyZbmzf1ZiM0P8wygM+94Xg8yMAeOYZfZ7rikuPHkYCAy0kJWl47jlvZn7aibeWr+JouXmkVZ3G1AMfMWzjMCzS425XB2/uY9hNmJ1uxdfX/g8VS4ok7UIIIYQQItehc1+SaoMLFgee3f81M4/N5LvjX9stnloOFurowMla71/LeXoqzJiRSmiohTUHOpForobaqsfl5r3tu63PuMT6TDNnjBrORYfTuLGJJk2Kt6e3rHJ2Vqhd2wwWZ9avd2b4cC/OnHHA0VGhb9/8K7w/8YSR0aNzHoj85z+eHDW9gmPwMJ707MmfSz7Fzc1Gnz6G+xKrVkvu4nabNzuxeLErP/5YjraDhnEyqiJ/XP2DaxnXOHxj131pXxTOyYTz/JgBuywGSdqFEEIIIcRDSFHoYznExRD4b63BjKv7IjXKBdAzeR5a/cUSD0dlTmNpBQsnQsDboc0dy6vVOT2xoOLLv57jhzQYs3caiq3ow2jP3txJj1joFqPGpmgYN+4ee+4fMo0b5zzgmDzZgz//dMbBQeH999Pw8ip4q7y3306nRYts9Ho1I0b6EF3hI0YsWIHJ4kj//lm4ut6/LfZGjNAzZUoqr76azquvplO/vonMTDXjRlfgs4bvcKxuM3rGTgFFtvmzl7oadz7xgfoZte7r70JpI0m7EEIIIYQAwCHjGA76M1TWOfL8s5+x9tNh7K9Zi3YOGXidGoEuZXfhKlIU1Kac7bq+Pf4t0w9PJ9uafdfxaLKjAYhP88OvglOhrhkwIAsXFxtfrOnLCwnwc2omRy/Nveu2b1Gyb1BPB0FmLyIizHTqdPev42HWqNHfoxJq1zbxxx8JDBly+33UtVqYNSuFChWsXL7swLPPerN5c868iKFDi3cBun9ycIBnn83iP//J5D//yWT+/GQCAqxcuODAlu+fpk72SRz0Z3FM2X5f4xC3Vx0tb3hDjcxm92WaRGklSbsQQgghhADAKWYRALujenMz2YcN6134/fp0rA6+OBgus3Bbfyb+3oK0tNO3r8RqxOd4P/wOdQZF4VDcIT4/8jnfbB+OOjv+ruLRGnOS9muJIflWjr8dDw+Ffv0M6NMq8bi1MZ/5QkP9jrtq9391cNVxLAQGR/fh+eczUcun57vSuXM2ffpk8dZb6axdm0jNmneeE+7ra2POnGR0OoW9ex1RFBVt2mRTpUrJLjzm729j9uwUtFqFpSsrciB5KADaq9+UaBzifxhzFqIz4mvnQEqWvO0IIYQQQggMFgO1j25ndEYt3l4yMvf4K5PqcLnGdmL9n2ZSMsyLu86+HT0od/WLnL3c/2HrgdEYkvaiMcWjNsXzaKVHaeJZiUlsw/foE2iyLhc6ph/iY3E7FkKvw40KnbQDPPtsTo/s8bkLedULKqTtQJ0dV+jr89BfByAyPkx62YvA2Vnh669TeeGFTBwcCn9dgwZmpk5Ny/351r9pSWvSxMSkSek5MUx7jbcSVYQe3cPl6DV2iedhtzx+OB1nLOVkcmd7h1KiJGkXQgghhBAcTzhOlP4mK25msvvkI1SvbiYszEJcnIZPPq8ENT9j4SPTGerjxzPlzLhd+xqNKQGD5e/E/cTFOTxz6i/qXofzEV9hcwzgqepP8Xv3RTi7hKA1Xsf3aC/U2TfuHJBiw9O5PJlOscQaXQgMLPyq3RERFtq2NXIutjorIqeR0HgTNseAotwWlPScpD02vTLe3g/PwlelwdNPZ/Hee2k891wmnTrZbweDYcP0VKtm5tz1MA7pK5Jqg99OTLdbPA8TtSkJv4OP4hY5DYBP015nS4+niPW+ZufISpYk7UIIIYQQDzG1KQm3yI9pHtCYKS2n4LR9Oihqhg7V89FHqQAsWODK8eMONAkbyMe9j5Ja6zvSw9/G5BhI7zW9GbtlLMkJu/G6/hWhDtDSOwy3wCdz27C5hpPYcDXZLtVZlpTMnB0jbh+QYsMl5kf8DnaiZbm2sOtNuNiNwMC7Gxp9ayXwUdNfYUtSOq/teA2T9e5XfW98WcF9W3POWsMfqjm0pcWYMXomTUpHo7FfDGo1PP98zqr26Rs/Y3UF+Mz5Mpr/n74h7p84YzpXtEG4XvsK55vLMVtz3gfKu/nYObKSJUm7EEIIIcRDRp19g7hrS/htzwg42AO36zNxj5xK9YznuLH1SVxccrbWatPGRJ8+OXufP/OMN8uWOWNTVBj9H0cfNJJDcYc4nXSardf/wv94f5ppkjlYLYw3Wq1k40YnPvnEjYEDfXj+eS9ikgL403sog+JgyrXjxMbln2ceH7+TCSvrknHmbRyyzqOOXARb3ydA3wGnwq1Dl6tDh2xCQiykpdt4btPzLDm/hD+urL2rOrLMWVzMPkpG4D58fcrfXQCiTHniCQMVKlg5sP0p6mnboVHZcIldaO+wyrwfzi+n2qE/eTEBtGdf4zuXGgzeMpeqnqH2Dq1ESdIuhBBCCPEQcYr/nfJ7G7P36Gu8cHoDQ65EoXeoiL7iIBYtcgGgVy8Dbm452ylNnJhOeLiZhAQNEyZ40bOnLwcP5kxOblahGet7refb6m3x14KiciCp0nf0eqw2w4d78/XXbuzc6cjvvzvTs6cfOsNIenkHMNEbQm7O+zsoqwG3yI95c9NTLEtJ4eVEDalVprAr8QVARVDQ3S9AptHkDGvG5oDfmWEMCwil1c1v72q7Lq1aS/f4P+C3BVQJcrvrGETZodPBc8/l9La/tnAyyVU+Jb3SeMw2s50jK9syzZloVVosjhVpes3CB6zl21Ej8fOz49ALO5CkXQghhBDiIeIa8xMADdwr0NbDj6r+zbhQcwHxhqqsW+cMkGdLLj8/G5s2JfDuu2mUK2fj+HEdvXr5MW6cJzExamr71uaR5nNJaLiO+IZ/8OLENly/rsXPz8rAgXqmTk2lalUzN29q6N3bh8HlfuNtbzW+KZvRZEXimLwD/4Mdcbs+k+m+Cm3cvXmp/SKygp7lepQOgODgws9n/1+3tn9LWfsac91vUs9yFl3awcLfq8zjjPJazUAXHZUqFS0GUXY8/XQWHh42Vmxrz8T9Fei4qgc/nPrB3mGVadMDA/lJN522Xt+RqUCGDa6ZwffhWjxeknYhhBBCiIeFNes6Dql7AajccjVL+x/jnc6/orHWYfBgb0wmFXXrmqhbN2/voaMjPP+8nl274nnqKT0qlcKqVS60bevP9OluGAwqzO71+XF1YzZscMbBQWHRoiSmT09j2LAsVq9OpF07IwaDmv4jm3PGaQrX6q3ig5NLWXT0E7TGa1gdK1Ch0Q/8POAkVSu05eBBHYsXuwIUqacdcrZ/69vXQIrem78uDQDANWb+bcs7xa/GKf733J916YfpVXM2TzRaTWhoyW43JkqfcuUUnnkmZ62E3zdncj7lPMsu/Ixild72+0Kx4hb5KU9Vepkpr9Xk42rfsN3fjdNH++Pj83D9/yhJuxBCCCHEQ2LRlS1UjvXnC4eu2JwCAYiK0tCrly8nTujw8bHy2Wept73ez8/Gf/+bxvr1iTRrlo3RqObzz91o08af7793ZdIkdwDefDOd2rX/7pl2d1f46adkunc3YLOpGPnZBC7ZXFl1eRVvXD3LtfLPEt9kG0bfLiQnq5kwwZNevXyJjNTi5WVlwICs24V0R7e2Cntr/stEm+Gjc2tITTuXr9zFK4uIOToW7zPPozbGArDvxkHW6+F4YoD0tAsgZ4FDR0eFqHXDGB/Qn90Vs3GN/8XeYZVJGmM0GlU2RpMjF2Iq884rz9Fg/A0Gfv0zvr4P104OkrQLIYQQQjwkNl77k+tZ8VzKbs6KFc5MnOjO44/7cuWKluBgC6tWJeZJtm+nTh0zK1cmMWtWMkFBFm7c0PD++x4YjWratjXy3HP599TWamHy5DScnBT27XNk3tY9lHctz6xOs3GoPgWruhyLF7vQpo0/y5blzK1/+mk9O3bEExZW9F61qlVztn87erUBPaJdmZKssPLIpDxlzOZ0Xtr1Lg2vwy8ZoDXk7CX/xZWDdI+FXeqUu169XpRNfn42+vfPAosT1c7Wx8d0DdeoWaA8XElkSTgctZ7esTApxgubouH6dS2ZBldA9dBtvyhJuxBCCCHEQ2Je53kMdZ7Ht88/x/jxXsybV474eA3Vq5tZtSrxrpJjlQoee8zItm3xvPZaOs7ONsqXt/LFF6mob/MJMzDQxvDhOYt5Hf32TVb1XEPnkM6cPq2lVy9fXn/dk9RUNTVqmFm1KoHPPkvD27vwC8fdzq3edrfTg2jnDA1NJ8H29/ZvqsjpVNaYcVdDtXY7MHm1AaCy2kJdHThk1cPB4Z7DEGXE6NGZqNUKr88Zh0XlhkPWJTQJG+0dVplzIu4gq/SwLUvN+PEZODvnJOqenraH7v9HSdqFEEIIIR4CrtFzCbgyFePmBpBZgVq1zIwYkcmMGSn8/nsi5csXrefK2RnGj8/k+PE4tm+Pv2M948Zl4ulp4/x5B+bPd2XyZHe6dfPj8GEdrq42Jk1KY8OGBJo0Kb55wh075mz/tm/516ytUJ4ejmk4J6wDQJt5muAb81lVEdY88iFfnf6JHqt6YLFm87WvgeMh4JP9WLHFIh58lStb6d7dSIbBne/P9eHJWBi69RV7h1XmdHR14Es/iLjZkp49DUyfngpAjRoP3xoCWnsHIIQQQggh7jNFwTVmAVrDFbSZHVGp6rNwYRIBAcU3xNTVtXA94p6eCi+9lMEHH3gwaZJH7vGePQ1MmpRGxYrFP+z11vZv77/vwfSN7zB+3A1+STMy70AfujlmMklnJdOrOy/95xWOdQ7CqkvmWNQfVFKZMFu0OHmXBzKLPS7x4Bo7NpO1a52ZsnA0cc/8iEIasbF/ULFid3uHVmbUsCTRxBOeudiTypUt1KhhoUaNePz8Hr6pKtLTLoQQQghRxl2NWUePS1dYkObA6sNP0KqVqVgT9rs1bJg+dxu30FALixYlMXt2yn1J2G8ZMCALZ2cbkxe/wKaY/3BZn8D+m/u5ZFKwaT2YunEGhw850/jyEyyp4E3ruG8BuJoYSkjofQtLPKDq1TPTqlU2N660YIJjE05Vglopq+wdVpmiycpZWyLFGoGTU86xqlUteHnd+5SZB40k7UIIIYQQZdyas7P5Mwu+ueGDPrscvXsXfTX24uDoCMuWJfHVVyn89Vc8jzySfd/b9PBQ6NfPAMAPP7jSM6wnszrOYmDt59lV7i8+m1kFgIqXe/JUuWTWpybiciycpts7EBIiK8eL/MaNyxl9sXXuXGo6glPCejTGaDtHVTYYLUbejvqYNtOXYXGtbu9w7E6SdiGEEEKIskyxMtzhOh/6gPrgKHQ6hW7djPaOipAQK08+acDZueTavLUg3caNTugyIngs7DHqBPVh3Bt1sVpVVKhg5a/TnTBbtVzKjMfgeplUxSZJuyhQ27bZ1Kpl5vDluvx54y2S6i3D5FDe3mGVCRdTLzLDOJxdHV8gKFRn73DsTpJ2IYQQQogyTJe6jwgSmeDuybG/3qFTJyMeHg/f8FLIGVrbpk02NpuKKVPcWbPGialT3Tl1Soenp42lS5PQZ7ux6lxTdCpoHN0ZDrxApUoP3xxacWcqVc7cdoCB095h4rkdtFzeCotNHvLcq0xzJs6GMEioSXi43E9J2oUQQgghyjDn+NUA/Ha4DyaLI716GewckX3d2nJuzRpnxozx5vvvywEwcWIaEREWmjQxMSO+HG8nQaVqf9LIwxF394fzIYe4s549DQQHW0hJdGL2qe+IzowmLivO3mE98B7RxPNpUn+a7ZpKlSqStEvSLoQQQghRRtkUG+9dv8RWgw9z/3oaNzcbHTrYf2i8PXXqlM1//pNB584GmjfPpkYNM0OG6OnfP+dhRseO2cTuf47mTtDKGaqFpdo3YFGqabU5+7ajaGh3fRDzajyKjyXJ3mE98HQ3/+CFRz6hZcR+6WlHtnwTQgghhCiz9t/czxdX9zPT6oH5Qgv69zGW6Bzy0kithldfzbjt+U6djHz0UR++TuxBefNN9jhXAx7u0Qni3w0caGD6dDcmVrhKe8t2UrJ7YaCuvcN6oCnpkQBEpUXg52e/nS5KC0nahRBCCCHKKA+dB73DnmTdyopgcaF3b+kBvJOqVS0EBVl57L9rAXjppdsn+EIAODsrDB+uJyo5GABNdqydI3qw2WwWOkWeJcQBMl19UKnsHZH9yfB4IYQQQogySGXR00CVQG/lG0yrv8LPz0rLlvd/a7UHnUqVM4T+Flk5XhTGsGFZXEzx45ARTtw4Ze9wHmjxKcc5kK3wWyaU86xm73BKBUnahRBCCCHKIKekP/E58TSNU3sB8PjjBrQyxrJQOnb8e95/SIisHC/uzNvbxi6XaJpEwcdX9to7nAearzWBtRVhoq4CVcId7B1OqSBJuxBCCCFEGbT7/FziLPDbnkcB6N1b5mUXVsuW2Xh62tDpFFm5WhSaszmCChrwwGzvUB5oHqYYerhCrfjmsgjd/5PnrUIIIYQQZYwxK4aBF45hUKDSiaaEhlqoX18SicJycoLlyxPJylLLIlii0CqZHmdd2EdkWdWk2juYB5gm6woA529Uo8UTkrSD9LQLIYQQQpQ5qdHLqO0IFdU6rp7tQa9eBlnM6S7VqpWzZ7sQhaVxqwiAiyYJrDKypagWGDtQ4ZPlfLP9WVlT4v9J0i6EEEIIUcbU0u9mXzD0OfwuoJKh8UKUAM8Ad1pM2sOoPy6A2tHe4Tyw3j/yFjef7I+1UiyOchsBSdqFEEIIIcoUtTEGx7R9APy6cxh16phkXrYQJaBiRRv7wlayzOdZjiYct3c4DySbYsPLUg1SK1HVO9ze4ZQakrQLIYQQQpQhqTG/YVHgSExropOD6dVLetmFKAkVK1qhwhEyvHcTmRZp73AeSLqsyyxy8+Q/51+mRoiXvcMpNWQhOiGEEEKIMmTUuZ2cSfDEf0MfVCqFxx+XpF2IkhAYaKVe9KPUcXKlrc545wtEPg6Zp2kasJLsxnHsdx9u73BKDelpF0IIIYQoIwwWA+dTzpNsTuXcyV40b26iYkVZ/VyIkhAQYKW3p5GFbdcSmHLC3uE8kLRZl4GcleNlWs/fJGkXQgghhCgjnLXOHHz6ICGbt0FqZVmATogSpNVCuiUIAFvmDTtH82Aaf+JXGl2HdQaL7NH+P2R4vBBCCCFEKeaYvA3XmAXM1nXlSMJxBlUfRG3f2gWW9TzzAhnpNlwiP8LBQaF7d0nahShJmVpfDhshynKRpvYO5gF0ND2Oo9ngklUBb28ZJXSLJO1CCCGEEKXU/iOv0iZlKU4a+CP1Cn8mXKKmV0TBSbtixSlxAy42A4ryMR06GPHyUko+aCEeYqnuZhpHgbc6ipP2DuZBo9hY6K9w0Qxz9ndFpbJ3QKWHDI8XQgghhCiFbNZsnju2lOFxkFp+ML0DqvOWt5YWhv0Flo9L2EOX6wbejHPgwo2qsmq8EHbg7VKPChqo4qBgMSXbO5wHijr7BrUcjfRw0uLpUd/e4ZQq0tMuhBBCCFEKxSfuJckKa/SQHD6J3rFrCMhei9EhnYJSgZMxf/KXAa5l6XB2UfHoo9klHrMQD7uKAeU5HeCJl2sq8eYELDpve4f0wNBmx2KxabkcH07lcOlm/1/S0y6EEEIIUQpVVhLICIeDNeuwf48XI8Y3AkCVcaXA8k21emb7Q5P4VnTtasTZWYbGC1HSAgOtRCUFA6Axxto5mgfL/mwt4cu+pc2338sidP8gPe1CCCGEEKWQQ+YpXNWQfrU1T7/ug597dUwKROqjKGfWo3FwzVO+suUK1T1g58nBdB4ie0QLYQ8VK1rp/+5ynMo588dW6R+9G6sur+J647lg/g/h4dXsHU6pIr9JQgghhBClkEPmaQB+WpuzBrXVwQufy1DzOtxI+se8dsWKQ8YpAA5faUSDBuYSjVUIkSMw0Mr5Crs43noIc078ZO9wHiheShhceQT1zUaEhFjtHU6pIkm7EEIIIUQpNOKGlh57W7HjSh3mzk1mxHADAYozripISDmVp2xqRiTLM305k+5OYnYVKlaUD7xC2IO3tw2tVzSE7ORwzFl7h/NAecm0g59cAmmhbomDg72jKV0kaRdCCCGEKGWSDEksjt7JHz570BNOt25GwsIsvJ7Qg4xwaOOad4bj3qRLPBVznVrHK1O7jk22ShLCTlQqaGiry9OxjzFc52zvcB4YKquBIDYwpPUiKgbLDO5/kqRdCCGEEKKUUalUdFa9D/tfom41JwDCwiwcPdeB9Scfw+IUnKe8VbHikV0DoptRt64MjRfCnpr6urK43RraqbbZO5QHhkp/EUWBpAxv/II87R1OqVPqHmNs2LCBNWvWkJqaSkhICMOHD6dKlSq3La/X61m6dCkHDhwgMzMTPz8/nnnmGRo2bFiCUQshhBBCFJ+A7Cu0uNmcPdtaUHtMThJeubKVWZufZ9bm5zk5+Abe/L06fM/K3Zix6lnSzmqpOy/FXmELIQBcK+b8RxWDQVGQoS93tjHyV8ZHQmurhg5VZOX4fypVPe179uzhp59+om/fvkybNo2QkBCmTp1KWlpageUtFgtTpkwhISGBCRMmMGPGDEaPHo23t+yHKIQQQogHl9v1r5ncugvD2i6gdu2cpN3VVcEvLBq6vchLW1/+u7BiI2B3fX58uiUBHnHUrWuyU9RCCABHrwAOGWFdlgGj/pq9w3kgXE45S6oNkgzust1bAUpV0r527Vo6duzII488QlBQEKNGjUKn07F169YCy2/ZsoXMzExee+01qlevjr+/PzVr1iQ0NPS2bZjNZrKysnK/DAbDfXo1QgghhBBFcyz+KBk2OHatfm7SDhBSSYFmM9mW/AtGUwYAGv1lNJZkagWeRtF5U6GCzV5hCyGAgAo6ekfp6HMDDl5ZbO9wHggventzohLUinxckvYClJrh8RaLhcjISHr16pV7TK1WU6dOHS5cuFDgNYcPHyYiIoJ58+Zx6NAh3N3dadWqFb169UKtLvh5xG+//cYvv/yS+3PlypWZNm1asb4WIYQQQoiiMmRF0zIyEYAK2eUJCvp7JfhqQV40dvCisVcKWv150DVm06VFvHcFWpm9ZBE6IUqBihWtVDpUnUrlT+CYKSvIF4Zj+lXqOIIxsTVeXsqdL3jIlJqkPT09HZvNhqenZ57jnp6exMbGFnhNXFwcCQkJtG7dmrfeeoubN28yd+5crFYr/fr1K/Ca3r1707Nnz9yfVfKXTQghhBClSELibipowGTTEFqxEipVUu658DArnYy1aOOxi2RTDEYacyL+EFctUD7Dgxb1ZBE6IewtMNCKx3+n8sdrj2FRRRJv74AeANnZYFWrMTmG2zuUUqlUDY+/W4qi4O7uzujRowkLC6Nly5b06dOHTZs23fYaBwcHXFxccr+cnWUrBiGEEEKUHjVIISYMpif3yDM0HnJWkL94MwIArSESbCZeL5fE5kDwOTtQ5rMLUQpUrGhl57l2mC1asBhQmWVxyH+TYkzh8XPdcPxsAQ4+YfYOp1QqNUm7u7s7arWa1NTUPMdTU1Pz9b7f4unpScWKFfMMhQ8MDCQ1NRWLReZCCCGEEOLB45B5GoDzV5sWmLSfuxnOZROcSziKa8yP+JqiqGX2Z8e28bLdmxClQLlyCj4BzlR99QKLjSdIUzT2DqlUu5BygX2O07A+MpGwcFmToyClJmnXarWEhYVx6tSp3GM2m41Tp05RtWrVAq+pVq0aN2/exGb7+x/3xo0beHl5odWWmpH/QgghhBCF5pCZ81non4vQAVSqZGW7Jp4q1+CF87vRXPkCgHdXTMHZzZXy5eUDrxClQevW2VwN2spzl6szZf8Ue4dTqrk7uuMZORzO9JVF6G6j1CTtAD179mTz5s1s27aN6Oho5s6dS3Z2Nu3btwdg5syZLFmyJLd8586dyczMZMGCBcTGxnLkyBF+++03unTpYqdXIIQQQgi7spnA9uB+6LParDSOVFNjVwuOxtXI9wHWwQE0WU1wUsFBg5Fq17XMSGjND9uGSy+7EKVImzbZoPfHpE7leMJxUO59cTVFUbiUegmlGOoqTZqkrmV9+Fn6ZzSmiuzRXqBSlbS3bNmSIUOGsHz5cl5//XWuXr3K22+/nTs8PjExkZSUv+eE+Pr68s4773D58mVee+015s+fT7du3fKsQC+EEEKIh4PKnMoHq+oz7OcqxF5betfXm7OiuXL+C1Csdy58n0SmRXIi4xznvE4QUL4CBQ0c9KQrGeFQWwexhiQWn3gGm6KhXj2Zzy5EadGqlQmutued+J4c8I1Fm3Xxnuv86MBHtFvRjlknZhVDhKWHLfE4zavsxatcGsHB9nv/Lc1K3Rjyrl270rVr1wLPTZ48Od+xqlWrMnXq1PsclRBCCCFKNUXB4+LbvOiaRsdU6LflVQ63icQY9gaoC/dx57U1bVmRns0vxjRa1Jt8X8O9nfKu5ematogNOzKpXavgoe5BIU4s2TWET0Jc2FO9PJ999CwAzZpJ0i5EaeHtbaN2VR3t3I0425IxpezC4lrwlN/CsNmsrI5cDUDttHWos/tgcwwornDtSp9xEU8NZFClwAeVopT1tAshhBBCFIVz3K+4xK/GXa0mzMWbL/3AO/pbdOmHCnV9ZuoJVqRnAxCUff5+hvqv/FK30l6fSdCV7vnms98SFmbhmVk/8cVfMzj20xuYTRratMmmZUtJ2oUoTdq0MbH5dEcAdCm7ilyPypyC/5EeHO/4PturVWOA7SjloucUV5h2ZTKlERIZjV8kGG6z+LiQpF0IIYQQD7jMrJtcPfUOAB4RE1g+4Ditm35LeugrmDybcyHlArGZsf9aR4XkdZwJgem+UL3ioyURdoHKRc3hjXZjaFV1978m7QB79uj44w9nNBqFyZPTUKlKMlIhxJ20aZPNxgvNeD8Jnjq1GaWI6214XP4AXeZJ3K5MI9vwNgAusT+Via3kbiYdwAoYbFCxvOzRfjuStAshhBCiROhSdhOwuz5OCeuKtd73Dk6j2TUD39oaEO3xEqkpGoz+T5AZOoEMUwbD/xxOl18f5dyJ90ApYMi5zYLLzRXU0MGw1t+jDxperPEVlmKzsCz2JMez4URMbapX//ek3WDI+Rg3dKie6tVl8SYhSpumTU2cjWrCJ8mwOtPC2SsL764Cxcbx0x/xyenlGG3w9urv6Tx8CGfTavB5op4/Dr1xfwIvQVU12WSEw1ylLhFVZPeL25GkXQghhBAlQn1xCmpTAm5XPi22OrPMWUSmRWJVbOj8J9GufUUaNy7PwoUuKAqkm9Jx0TrjatNTL+EHvE8+g8qcnKcOx+StaExxZOBD/zkOfH5gJraCkvv7LCHpAENvmml0HWxuQTg7F1yufHkbzs458Xl62pgwIaMEoxRCFJazs0Kj+g50SGnKPH+on7mjcBcqCo7J2/E61I0xB77hw2QYmVCb6T+2B1QM29mM1xJhyvn1WE1p9/EV3H9aw2XKqSH7Rn3Z7u1fSNIuhBBCiBLx7LUoXC7DD6omxVani9aJ1Y+v5sCAw/wwuSuJiRqys1W8+aYnY8d64a4EsfqJ31nZ6hW8HZxwSt6C36Eu2JL35dbx/plVjE+vyMz9j7PZexTTj3/MjYxrxRZjYRnSjtPOGWrjRq3qutuWU6vJ7Vl/5ZUMvL3L1vZPQpQlrVtnc/3XuQz3AJ+Uzaizb/5r+dTsVDzOv4rPiadx1p/iPV9HevpEsPmr9QA4Oioc/OUrmumcmOhlw+XG3e+UUZpkm3VcTQjhbGz+LS7F3yRpF0IIIUSJuGbUY1TgYEYyS84tufcKrUb897fE4/zr/P6jL3v3OuLiYuOFFzLQahV+/92Zrl39uHjGncCIF0lo+DsW51C2pcTSfPWTrN3eG2PaaX6I3MSXcbG8vXoorRVfBruBNmXvvcd3l2qrktkWBM9dHXzb+ey3fP55Kl99lcKwYfoSik4IURRt2mRzKqoOvxwcQHrIBBS1423LfnHkC5osacJmqx+KSkdm0Ch6djlIud1HuXk1iLAwC19/nYJidqPFrs941gPckv4owVdT/CZcMFH51xHMPd0fDw95AHk7krQLIYQQ4p45Jm3B6+Rw1NlxBRdQbBwIVpjjD/Mub+TDfZMw6q/fU5tZN1ahGKJQ39zGR59VAGDKlDTeeiuDX39NJCjIwtWrWp54wpcffnDF7FqL+Ibr+SDDhxgLXE04QPCRrsxqNoEqqcPhWjteSGvLwvIQpi75IecOmacBOHat/h2T9qpVLTz5pAG1fJITolSrW9eMh4eNfl8t4oMztXl93ycoyt/JqdoYg8e5V3CJXUKiIZEsSxbLk5OJa7aLtPDJLP0tiJ9/dkWlUpg+PZXu3Y3UqmVm+d7eXM1sgtGnMygPbrL7W+w86DCRwCoJ9g6lVJO3eiGEEELcM5+TQ3BO2ohT0uYCz6tNcegwM9RVTW0HT4a7ZqGOWXxPbU45NJ2KV2Ds3kZYLBqeeCKL/v0NADRqZGbjxgS6djVgMql47z0PRo70IlXvwYInD/BRg9GMrdwaXCNoGjSa2O/nACou3MjZR1mbFXlPsd01RUGTfhKAo9caUKvWvyftQogHg1YLvXsbQJfBjKgxLDq3iEPxh7CZUli1czCWXa1xvfkzblen83K9sczpNIdpraex7UBlevTwZcIELwCGDdPTtKkJlQpeeimD2JRA6r21laXGGpxIPGnnV1k0VquFevoX4dgz1AyobO9wSjVJ2oUQQghxbxSFJ25oGR4HUdrAAotoDVEARCWF0mb3F3zhB0FJv4JiLVqb2fHsSY0l0Qqbd/XD19fKJ5/k3fbM01Nh7twUPvwwDZ1OYcMGZ7p08eP0cTeeaTwRdaNlJDZczerfXcnKUlOhgpXzN6oBYE6/VLS4iijdnIHXBQtee+qQogrD0/PB7TkTQuQ1dKgejF6oDjzPmOAu1D/7LO+uasi4c1uZkmQi26MFybW+x79cIIHpT/DUU7489ZQvx4/rcHXNWWxy4sT03Pq6dzcSEWEmvdFkxuwYyrzT8+z46orOOf0Aa6t+y6KKZqqF3X7agJCkXQghhBD3KCvrOr9nWpifDgcyUricejlfmYOJpxgd5cMPN335YdNAjDYvtNmx6BKKNh/TNWENZ0JgbrkIoo49zZNPGnB3z5/oqlQwfLie1asTCQ21EB2tpU8fX779thxmMyhaN5YudQFg5MhMLmpsBEZCo9MHixRXUZ1JOoPelk6qQxpVq7qUaNtCiPurWjULzZplE37iOb5z2kgIKYxyM+GpUVMpdBBJ9VdwNqEpzz3nRffufuzc6YiDg8KIEZns2RPPK69koPuftSnVahg5Ug8Xe6AzVKQSGWDLtt8LLCJt1iW8nOPxck2hShVZhO7fSNIuhBBCiHviaLzCsvI534/cMo4FhyblK7Mr08D3xiQ+uliVbLMTK48N46gRWv8xhsOHXwKr8a7adL65Aq0Kjv/1Eihq+vXL+tfydeua2bAhgccfN2CxqJg61Z02bfz5/PNyHD2qw8FBoW9fAxp1fWKtcNVsxZydclcx3YtGAY3oeGEfrJ4vQ+OFKIOGDs3iwo1qLN43AotjELXqTOHAoJN0qfRfXn/Dkw4d/Fm3zhmVSqFv3yx27ozngw/S8fUtePvJLl2McK015ytomM5GHFNLfvHMe7Xx/HxSrXAwsokk7XcgSbsQQggh7omb8Tr93eAH12Dc1eCTuj3fXui1vOrC/hfgUjcAnv92Mm+l+nHeDIsurMTvUGc0xuhCtRcftRJr+kmsigNLdg2kTh0TNWrc+QOfm5vCt9+m8N//puLnZyUqSsv06e4AdO5sxNfXRpB/OOu8PEkIAxdT4eIpDl5R39HP41eqGSvecRE6IcSDp1s3Az4+VgZ/PZcFKceJcR3OjP8G07p1AIsXu2K1qujc2cBffyXw5ZepBAf/+9QhPz8bjRra+PNkVwCcEjeVxMsoNtE3NvHU5QsEX4H5h3sTGFjEqVIPCUnahRBCCHFPtFkXAYg/0o/9PrX51NeGc/zvecqEKu1h/dc4XXyKbt0MZBjc8Tl1jLHhnfi8og9onLDqymO2mfOsrPxPBouBAfu+pslNb6YdeIakTF/69TMUOlaVCp56Kou9e+OZPDkNPz8rGo3CiBE5W6dVCbdy9ehTHIsa/K9bMxU359ifGd36UwK9Y6SnXYgyyNEx570HYOpUd1q2DODbb90wGlU0bZrNqlWJzJ+fQvXqhe9x7tzZyJojjwEQf2P9A7WKfNb1n6itg7Ds8rg410KjsXdEpZsk7UIIIYS4J/tuHuJYNpyOrcYPG0cA4HJzRZ4yta6249hH9Whd7yzPPJOTIP++ojwvN/0JdcudJNecBWotnxz8hMHrnybu6sICP4BeSbtCcnYKN8xa3pk3Ba1WoVevwifttzg7K4wapWffvjgOHoyjWTMTAOHhFsYt+JYJy+dhca161/UWhTk7kQ9ir7EyA66m16RChYKHwwohHmyDB2ehUilcvaolPV1NjRpmfvopiV9/TaJJE9Nd19e5s5G/zrSj3lUVYRfiiL5Z8O4dpY06O552xl0crwSalYsIC5Oh8XciSbsQQggh7snI60k0uA4bU92Yv3UQVkVLXPIx9ClHALBZssgyn6dupRO4+7rRurWJ8HAzmZlqZs4sh1XjgdUljGRjMj+d+YltMTuIPvUm3ieH5g6ZV1kycY77jZpeEWzqs4n2N5aBPoCOHY34+BQ9yXVygoCAv68PD7eA71nOen/B8vPL7+3GFFLkjb+YkgzD41QEVvbMswK+EKLsCA62Mm5cJvXrm/j66xT+/DOBjh2zi/z/fESEhfIVXFAM3miAM9dXFmu894tr7EJUionD11tw9HxHWrd+8BbRK2mStAshhBCiyGyKDWelEhi8iL/cnsQMPwZeDibkKqw++TkAcSlHqXgVvC6DR4A7KhW5w9G//tqNXr18OXdOi7eTNxt6b+C9Ku3o6abDKXkLqj3tmLyyGsOXVcPzzAv4HuuDt9qN3cvbANzV0PjCCAmxoKp4GFO7N1l88qdirft2yhmvMsIdWmRXok5t6XESoix7660M1q1LpE8fA+p7zMRUqpze9lrHx5EQBgOd0oonyGKgKAoGS/73Z6vNymoi2GeaxHvL3sPT00bfvsX7Pl4WSdIuhBBCiCJTq9Q8ZVwP05KoV8UblUrh2sVGKEBk8ikAElJOowY80BFSKee6oUOz+OCDNFxdbRw+rKNLFz8+/tiNio5VGPPIEhIabyLbozluGJmXnMmGLIjR+GP07con/w0gLk6Dj4+Vjh3vbtX5O3F0hEaeHgwsBwM0p0tkjmgt4pkbAC3OPyOL0Akh7krnzkaunO2GlwYcMs/YO5xcXx/7mmoLqrH3Rt5V7f+89idD/nqeTsfXs+F4VwYP1uPi8uDMxbcXSdqFEEIIUWQOaYcIyZ5L/ZBjtGtrom3bbKK2TORGZfjGKw2VRU9zVx2GKjA5vT2VKuWsEHyrt33btni6dcvZhm3mTDc6dvRj2zZHLK5VSKq/An2DZUysM4hZbT8jq/lO1lyewHfflQPgk0/S8uxdXFx8dG1ZHKDiVS8TalNC8TfwD9qMnIcbR682kEXohBB3pUkTE9fTatPvy+VsUf9eahajOxx3CKtiZeKeiXmOx+pjcdN4oT/VHq0Whg3T2ynCB4sk7UIIIYQoMufE9TxT61WGt/uBqlUtDBiQxc3YOkyYv46Y5sdRtK5ojVHoVJAaV5NKlfIO/65Y0cbcuSn88EMyFSpYuXZNy6BBPowb50lCohaTV2tGNf+Ux6o9jT7FnZdf9gRyPuh17168vey3BIdquJoYCoDWEHlf2rhFsVlIyIgF4Hx8XcLCZNsjIUThabXQoo2aXyypjD34BScST9o7JLCZ+ES9k6/94OcqVcGW876vshp4VdnFtLS3Ue98i8cfN8jCm4UkSbsQQgghiuyVU2toFw0b9SoiIsx06WLEw8PG0m3d2bnPDwBb+nUAriRUzu1p/6cuXYxs2xbPyJGZqNUKq1a50K6dP4sWuXDmjJaVK50ZNcqbpCQNNWqYee+9+zd3Mzzcwvkb1TDYICvt9H1rB+B6ZgxBl1NwOBSMa0CFe57jKoR4+LRrlw3VfueS83IOxx+2dzhosy7RSGfiBU+ok7IKXXrOoqS6lF04J/1Jt6CZ2LI9GDVKetkLS2vvAIQQQgjx4NqTFs+pbKiTWoHwcCtOTvDYYwYWrU3gvcPvEmK4Qe3sVPTJPsTaXHF1vf3QzXLlFN5/P50nnzTw+usenDyp4403PPOUcXa2MWtWCk5O9+81hYdb+O++aLpfhvdUfzImZMR9a+ty2mVQVFj0/tSpLT1OQoi7V6uWmYBlj1LTw0xbdby9wyEuYQ8J2VBVBw4qcEzeismzKerEPwFYd6wHzZubqFtXpgMVljzPFUIIIUTRWA0sDDCzKABUyT1wds5JyDt2NILZhctui9kStYWZ8ceZZ0nisqVGoaqtW9fM2rWJvP9+Gp6eNtzdbTRrls2wYXpWrkyiSpX7u8J6eLgFk9kZgFTj/V2NuUNwB5ptjYcVy2QROiFEkVSpYqGVJYQtbTbSMOUve4fD4ku/U/s6DI3V8oceZp9bAYpCmwPLaRkFyy/VYeDALHuH+UCRnnYhhBBCFIk26zL1HSHY5M0CnwggBYBWrUw4mMozzFyVAaEXWJzYifk7IqjiHVL4urUwcqSe4cP1qFSU6N7lAQE2mt7sxuomBzG4VruvbXmeeZE3mqh5L3KSJO1CiCJxcIB0TS0AnLIvkmYzgfo+rNJZSCZDPOVUEHmtMT2y9qHhBi2uLeV0tgUNoDvxGN98J3uz3w3paRdCCCFEkThkXQTgbGwNqlb9u/fb1VWhaVMTrofH0NEF/uNigfVfUyXQ/a7bUKtLNmGHnPbSDc35bedwIjPa3r+GFBu6xE30bvgLDlobVatK0i6EKBqvoArEZLhzKNtMRop957VPdTeRFg7Kpk9pqnJjpDsEx3xHVCi8aWhIrap+eHvLdKC7IUm7EEIIIYrkcOx2VmbArpuV8g1Zb98+m40nuwBQx3czP784gJCQ+zusvTglaNoxcs48Nl0eet/aSE09yZCYDD5K0GAtF46j431rSghRxtWsaaHzDYXmUbDn2lq7xaE2JeNMHGoVnL1en8dOvcSsAKhkjiTIAZIPj6BTp/uz80dZJkm7EEIIIYpkTqqZvjfhi1ifPD3tAO3bGzkbU4NtWfBGIoRW+5Pg4AdnOzOngOvQdgpbjN/ctzbOxf7Jz5kwK1VLjZolPJxACFGm1KxpxiMjmPIaMOqv2C0Olc3A3tg+/HnyUWrX17HheNfccyaLA+uO9ZCkvQgkaRdCCCFEkfg7RkB0U+IvdCEiIm/SXqOGhYAAG4/EwKcp8HSUCyEhD07SrvGIhQ7vccLx/iXtVUjlYx9okVpf5rMLIe5JzZpmqh19kRthMNTFfkPPl0fto/OVZLps6suQIVmcjmtKla++J+TMI7jPmI3NKZDq1R+cUVelhSTtQgghhCiSTg6vwdz9VMzsSrlyebdyU6lyhsi7/LiFiinViFu8mgoVHpykvbqnwkh3eMk37r61UdUaxZve4Ht6iCTtQoh74u2tcCOzPgCazAt2i+PQzcNk+m8Dr8vUqWOiZSsrlxvM5LrDVrLrLaBTp+wSX6ekLJCkXQghhBB3TWOMJihuIkNa/5Svl/2Wdu2MZF15hNgvz+GnrY9GU8JB3gM/9yrMCYApflawGu5LG5qMMwAcu1afmjUlaRdC3COPGjR85zBfXj1mtxC6lXsCVs/F8fKThIZa6dAhG1bPh1P94beFMjS+iCRpF0IIIcRd06Yfp1G5WbzU5avbJu1t2mSjVuf0wFeq9GANh3T1cMVizXnKoLakFnv92dkpbEl34KbelTRq4uam3PkiIYT4FxHV1RwN+5XP0h/jYNzBkg9AsdI39WnSx43nkWAfNBp45BEj3GgIvyzD2RREixay1VtRSNIuhBBCiLs299zPBEbC1DRzvkXobvH2VqhfP6cHuVKlB2doPICHh0KK3guzAkp2crHXH6m/Sdfo61S46Ezlqk7FXr8Q4uFTs6YZAk6Q5HSIkwknS7x9jeEKWpURtcqGT5AfAOXL23JHErVtm42TvN0ViSTtQgghhLhrF1IjibVCXKb3bZN2gCFD9AB07PhgDYn09LTRKiEd3SU4llD8ex6nZafhaPaHtBCZzy6EKBa1almocrUrY1Ie4Un19RJv/+qNrazKhL+iIqhR8+/F8IYO1aNSKQwZklXiMZUVkrQLIYQQ4q595OfMniBwOT2AKlVun3T272/g2rVYOnd+sIZEenkpqG1aAFKzin8xuuYVmhP0czTM3StJuxCiWFSubKG6qTLfNd1KaNKfJd7+H9c20vsGfKzPpEaNvx/mDh6cxfXrN3jkkQfr70BpIkm7EEIIIe6OYsM3+yotnCEzqR1eXv8+H1urLaG4ipG7u413DU1JCoOWTsHFXr/zuQ/4ZUQTnmq+QpJ2IUSx0GggRdMEq02Ns/Ua6uybJdq+pzmFBo7gEF+XGjX+fl9TqUAtWec9eQD/jAohhBDCnjTGaDQYyDbr0HlVAtLsHVKxc3CAU+cew5gUTtMBYfgWc/3WpPPUDzmOj4cBPz/77akshChbQqo4s/5qdZJ8z5C1fg0KL922bNOm2VSsWHzvP2Mc0/hPJeiz9iU8PGRxzeIkSbsQQggh7sqVuJ0sSAaf9IqER9g7mvtn0aGXiInRsuaJBHwp3t7w/0QdRgUYXB+gffCEEKVerVpmpiQr7LfCoylr2PTlpNuWrVHDzF9/JRRLu2pTAs7cwGZTYXarDZiKpV6RQ5J2IYQQQtyV7Vk23kwC1+vBvB1Rdod268L2Qvg2tsSE0rBhx+KrWLGxRp9Bmg36uBR3H74Q4mHWq5eBE/PaonE6S+uKcRha559Hnp0NBw86cvVq8T00tCk2Vl96lfioVMKqOiJJe/GSpF0IIYQQdyXYLRjXyAHoLzQjouuDtf/63bBV2g4RE9mX0gcoxqQ9+waz/CHSpCIqoUHx1SuEeOh5eirMeGE05ffORiGGUYuvoGjd85RJSVFRu3YFDAY1ZnPOdKB7Nev8b3yiX4b57Di+G1x2H+baiyTtQgghhLgrzXzbk7XwKVBUVK1asgsdlaRu/klY3KGq+lyx1qvLjmWgG1yJD2F5sCsg2yAJIYqPzTEAi1MoqNRojLFYyuVN2t3c/p5vnpGhxtv73ue1H48/idklCtQW6taVpL24yTp+QgghhCg8RcH12PN8MvANKlVIxte37C6iFkFz5gRAfxeXYq1XY8jZP/lqYijBwdZirVsIIQASmmziZtPtJGgD8p3TasHVNee9Oz1dVSzt/cejFx7LVuF2dSAhIfK+VtwkaRdCCCFEodmMsbgbfueV7tOpFKpBVTyf90onR08AnFQpxVrtZX0SK6MqcyQ2nODgsju9QAhhP+ujdlBnYR1e3vZygedv9bZnZNx7Oqg2JdJeP4zkt3vTPNyzbP9dsBNJ2oUQQghRaGdiN1HuMjS64kBIWNmeZadx9gBAq04t1nq/uBZNX+MVXo32JihIeqSEEMWvgmsFUrNTOZ10GsWW/+Ggu3tOT3ta2r1n2A4ZJwC4cLMqVWvp7rk+kV/Z/msrhBBCiGJ1NfEoCmAzlSMiomz3Euudsyh3CawkcVlRKK7uI0OmDjL9cbeG4uhYLFUKIUQetXxqsaNuO5pmHyQ94yhmjyZ5zhdnT/vWyFVcTQHT9XDq1JH57PeD9LQLIYQQotAGebkSXxm6XH2SqlXLdtLu7lYRvQJGBYzZiUWuJ9mYjNFizP35UWUK/DeOapnDiyNMIYTIR6vW0tjVBUdbFo5pB/Od9/AovjntK6L28FoirM1SqFdPkvb7QZJ2IYQQQhSaOvMCflqIv9aSiDK8RzuAn5cPpyvqSAoDF8V45wsKcCn1Ek2WNGHUX6NyDihWhrhW48jUBtSoXPQHAUIIcScmj6YA6NL25zvn5nYrab/3dLC91sCT5UCJ6iKLa94nkrQLIYQQotA0mZcAuJpcnQoVyu7K8QBeXgp7Dw5m/cHhoC7aRsa+zr4YrUa2RG0hPn4HmuyblNMmUivoND4VyhVzxEII8bd45xpMToJnzmwHJe/79d/D4++tp11tSuJ591SWB6hwtwySRejuE5nTLoQQQohCMWWn8PyNFGrrwFwuBJWqbCftnp4KXebMw9FR4fIbN7jbz6JqUzLhZ0Yy1QfqO0JQ2jbOGG7QPxrCLC7UraTcuRIhhCgilVs9piSDFTNvxO+kfEC73HN/D4+/tz7cPIvQ1XQETPdUnyiYJO1CCPF/7N13dFTV2sfx77T0HgIhhBY6hN4EFMFeQEVRwa6I3Wuv14aKihUL9t4VBOwoSFNBeu+QQBIC6T0zmXbeP/KKcmnpk/L7rJV1b87ZZ+9nNjGTZ3YTkQrZVbKf9wvdmMrCGNs2FCjwdUi1KiLCC91nUNZsC+v3H0/vll0q9XxA1nf4Fyzjwajy7925v5BsK2OxHbK8cHZnTSMVkdoT5B/GPS1b09adSmTpZuCfpP3vkfbqJu3plhY899Or7E8NYeiljXvJlC9peryIiIhUSKgtlM7778VYfhNdGvkmdADBwQb0ewdOeoTle1ZW+vlnVr3Cc3lw+8wHKHP5Y7XvZpiRwuexMCJ3IG3aKGkXkdr1QOJF3BABLeybDrr+95r26k6Pf3jlG0zpcBsf2Uu1CV0t0ki7iIiIVEh8aDyWRU/CFhsdL8/xdTi1zmSCy1vmEhAGnRybjv3Av5Tkr+XlzAwcBkSsG0nvdqsoSfiVDiXzGR8Km9NOJDZWSbuI1C5nxDAckStxhg846Hp4eM2MtO/MTgezl2BXG+Lj9TuttmikXURERCokbOv9TD5jHAMSVjT6497+NtI+hLdbQH9LWKWeC836gZdj4CR3K/J3nsIrmSHcmgWv5ZffL/S2waqhExGpZc6IwezqMo3Z7jgKnYUHrv+ze3zVR9pNrjxe8h9D32++o2/kUG1CV4uUtIuIiEiF7N77K2f1m0l4cGmTGVEpMyIAMMoKj17w3wwvMVmzuC4cms17AasVkhbdxRB/sJlgRYmVYmur2glYROR/nPf9eVz969Us2/fP0W9hYdUfafcrXMupkffxxRV306dH1U7YkIpR0i4iIiLH5HEV0j85g+BdUBARhMXi64jqhssUAYDhzKtQ+aSCJN5eejvesv0Ul4Xx3apzuPHGYhz7h/BowSnMLYVB6W4yQ5rVYtQiIv8Y1GIQHcLa4S7ZeeBaWFj117Tbist3jl+1u7/Ws9cyTcwSERGRY8rJWYG/CdwGxDfrChT5OqQ6sTJgGyE7YYDfH3x+tIKGgcfwMub7MWTbsxkUH8oPXz5NmTuAK6/MYPNmG9d//CKOUVdDWCqdY+MA/ZErIrXvxW6n0cL7Oa6SGWRxI/Dv6fFmDIMqTW1/dtNMsougOKMF/+ml32e1SSPtIiIickxtyacgAT53H0fnTk3nfHGrNZwSA4q8jiOWsRWuJXrdhdhc2YxuP5oR8SP4bPunvDHvJk44oYyWLb2cfbadPek98b69Ep7PoH1rjZuISN0wwvsCYCvZislVPmvo7+nxLpcJx5F/vR3VzKzdvFUI2+3NaNWqaSyZ8hW9Y4iIiMgxWUu3YzLB/vTedB7aNDahA+hiO54dbZ/D64w+7H3Dno513dX4ezIJTX6OJ4Y+B5gY9kRzAMaOtQNw2mkOrFYDt7t8OKt166bThyLiW16/ZrgCO2Cz78KavxxXzOkEBxuYzQZer4miIjOBgd5K1Wl25vJElJsVZbDKOFeb0NUyjbSLiIjIMVn/fy3k5r3d6dSp6UyD9AtIYP26MWxLH33oTY+Dl+aOYsiuTDZZ2rIjdBLvvhvC6NHN2LPHSnCwlzPPLB/Ciow0GDas7MCjOqNdROrSayWRdNgNr61/CwCzGUJDq74Zna14PReEwjWuTnTr0LYmQ5XDUNIuIiIix3TLzs3cuN/CuqJw2rVrOgmnOaQVF0ydyfMLXjj4hmFg2nIXX+dksM0F72Zdx6BhHXnssXDWrPHDbDa4664igoL+WUpw1lnlCXxAgEHz5pUb1RIRqY6ygHiSXLAse8uBa9U59s1WVL4J3crkAdqErg5oeryIiIgclWEYfJ6Tg8Pw0MHdq0mdLx4W7oaB09jWIhu7+zICrYEABKe9S3j2bJa3NjHNO5EXbrofl8tEnz5Oxo4t5eyzHYck5qNG2fnww2D69XNqKqmI1KnTOl9Bn7zZHBdYSqnHDpbAA+vai4oqP477i9GHZ1/9EE9GN966Ukl7bWtCb7siIiJSFR7Dw2meZ/luyR66tOgAFPs6pDoTEW7A6XeSYXWSU3Iq8eHt8MtdTNiuxwHwtHqU96/4L3a7meHDHXzySe4RP9SIiDCYNy+rDqMXESkXGz2I3pHNsTgzcRetwxlx3IFj3woKKv8p4t2/TyLljK2Efj+LuLj4mg5X/oeSdhERETkqq9lK8I4rYV4wXe9sGke9/S0qCi4M9CM80EmQI4mykJZc/ddr3BMQx+AWQznv/vtJT7eSkODmjTfymtQsBBFpQEwmitrdhWEOxBXcGfhnTXtlR9q9hhdLWTNwBtOzWS/NHKoDWtMuIiIiRxWS8jqPDR7C9Se/2aQ2oQOIiPDyX1MC77SAFhYzr697nV/3/sm4vaVc9+HzrFzpT1iYlw8+yCEioukchSciDU9S+ClMyy7gnW0zAQ6MtBcVVS7r9i9axzRzT0bN+YSBXWNqPE45lD4PFhERkaPavP9PosM3EBKYR6dOTeuosvBwL0klUQCYMhZzQ697SCpIImjHZXz6eWvMZoM338yjY8emszmfiDRMSQVJPLz0YWKDYpnQY8K/psdXbhzXP+8Pzkx4g/whedh7nVgbocr/UNIuIiIiR3X3juX8WQp9TdncnNC0knabDYocEQBEZ76FLTyOMcbbXPlEeSL/6KOFnHhi2VFqEBGpH/o278vJLQcwJDgEo2QnoaH9gMqPtJvzy3eOX5Xcn4uvdNZ4nHIoTY8XERGRo/J4nUSawb9sAP7+vo6m7k0NWo5pBzyfD/tym3HTTZF4vSbGjy9hwoQSX4cnIlIhgdZAfmgdzBOmhYTk/3FgpL2y57SfueE3Tk6D1WWRxMXp+Mq6oKRdREREjszw8me8l9wO0D7oOF9H4xPugkQAVjl6ceaN11FUZGbw4DKeeqpAGzCJSIPiDB8EgH/B8gNHvlUmabfb97LEXsZ8O1iC+ul3YB3R9HgRERE5IpO7CLOpfCSlRetQoOlNhXSse49Q72f8vP1aCjOsxMe7eeedPPz8fB2ZiEjlOMMH4TRgw/4/CQ0t34ujMtPjQ0t3sLI1/JrVnH0dmtYRoL6kkXYRERE5IrM7H4BiRzAJnSy+DcZHYoKjKfr9PgozogkK8vLBB7lER2tKqIg0PPmBnYnYBYOTcggOXgtUbqQ9oHgjfQOg3d4R9O7VtPY48SUl7SIiInJEuwuSOTk5hIlpQXTo0DT/QIuI+CdBf/XVfLp3b5r9ICINX1BAM9r5BxBjAUw/A1BYWIk57sW7gfJN6Hr1anozr3xFSbuIiIgcUYrHj/nuYr7MjKZZs6Y5ujx4sBOr1eDhhws44wyHr8MREamWHweMI6M9nBKaAUBRUcVTwntSehH5wgfM3jqW2Nim+Z7gC1rTLiIiIkcUF5AA370N7gAib2uaf6CNHWvn7LPtBAb6OhIRkeoLizkRU8aHRLhXA+Vr2r1eMB8jd7e77byb9hDeczz0WLZdm9DVISXtIiIickT+ZXGwuh82m0FQ0D5fh+MzSthFpLFwRgwhu88sCm29APB6TZSUmAgNNY76XKmrlDb549idn8rAri3QJnR1R9PjRURE5IhC0t9n3dO9eHDMcxpVERFpBAxrKC/sXsb5cy/AkrAIqNi69ta5P/JhRC5jN99C716u2g5T/qVejrTPmTOH77//nvz8fNq2bcs111xDx44dD1t24cKFvP766wdds9lsfPbZZ3URqoiISKO2r2AzLVpsILJZmq9DERGRGrI5ZzPrstYS2Gkh9qQT/39d+9GXQFlzlnBip5/5sfkIeilpr1P1LmlfsmQJH3/8MRMnTqRTp078+OOPTJ48malTpxIeHn7YZwIDA3n55ZfrOFIREZHG763Mv/iqGM4M3MyFvg5GRERqxJUJp3OeXz6eknVM/KVix74VZq8lxAxJeX20CV0dq3fT43/44QdOPvlkRo4cSXx8PBMnTsTPz48FCxYc8RmTyURERMRBXyIiIlJ9NsNFlBkCaebrUEREpIYMazmAqz2LubLn9wT5lxxzenyZfT9td6XSLhnsUfF1FKX8rV4l7W63m6SkJHr27HngmtlspmfPnmzfvv2IzzkcDm666SZuvPFGnn32WVJTU49Y1uVyUVpaeuDLbrfX6GsQERFpTJ4IjyOnAwxxnevrUEREpIZ4/Fvh8W+JzeJmUIflhz/2zWPH5CnPlXbv+xU3UOgx06WTkva6Vu3p8Xv27OHnn38mOTmZ0tJSDOPgXQdNJhOvvvpqheoqLCzE6/UeMlIeERFBenr6YZ+Ji4vjxhtvpG3btpSWlvLdd9/x0EMP8eKLLxIdHX1I+VmzZjFjxowD37dv354pU6ZUKD4REZGmxt+UB4DJ//BL1EREpAEymdgd2ItlWfuI7/8RBQV9D7ptdqTTbO35uIM6kdvrE/pbi8hLgLdXn0yLPlrPXteqlbRv2rSJp556iuDgYBISEti9ezeJiYk4nU62b99O69atSUhIqKlYD6tz58507tz5oO/vuOMO5s6dy7hx4w4pP2bMGEaNGnXge5O2whURETmiQHN50m4JVNIuItKYfFjg5ckMOKHdz8QXvXLgusldSPSGy7E6UilqdycP/fkQbQqX8lAA5Ow8mVOvUNJe16qVtH/99dc0b96cyZMn43a7mThxImPGjCExMZEdO3bw1FNPcemll1a4vrCwMMxmM/n5+Qddz8/Pr/A6davVSvv27dm/f/9h79tsNmw2W4VjEhERacouyXAQYg6gX5A2HRIRaUwGtzmLgXvmcqJ/PgW7POUXvU5emXManTypXBnTnOWeSD7Y/AEAJ8QEklzQhxYt9H5Q16q1pj0pKYmTTjqJoKAgzObyqrze8n/ETp06ceqpp/LVV19VuD6r1UpCQgIbN248cM3r9bJx48aDRtOPxuv1kpKSQmRkZCVeiYiIiPwvl9fFz85CpjscWEPifB2OiIjUoH7tLmBRXDhPtHASbmwCr4vVf13GM/tSmZAJF3zzArdfOZ6b415jOHdz6i3FFAYe7+uwm6RqjbRbLBYCAwMBCA4OxmKxUFBQcOB+8+bNSUur3Lmuo0aNYtq0aSQkJNCxY0d++uknysrKGDFiBACvvfYaUVFRXHLJJQDMmDGDTp06ERsbS0lJCd999x1ZWVmcfPLJ1XlpIiIiTZ5hGMSueJP9+QXE3RwKuH0dkoiI1BSThdSywVhzdxBp2UH0+vs4u2wJD0aaWLhjPN9+eRkAW6+7mZgYD4Zhpmcvj4+DbpqqlbTHxsayb98+oHxteKtWrVi+fDknnHACAKtXr6708WtDhw6lsLCQr7/+mvz8fNq1a8eDDz54oJ7s7OyD1qEXFxfz1ltvkZ+ff2Bt/ZNPPkl8vHY1FBERqQ4/ix+suhb2W4h6KMvX4YiISA2bU/wBt90fxvO3Pox//hK81mBCtn/AkmkXEhjo5cwzHcycGURWlgWAnj21nt0XTMb/bvdeCV999RULFixg2rRpWCwWFi5cyBtvvEGLFi0AyMjIYPz48Zx33nk1FW+tycrKwuXSD6GIiMjfbAUr2f/dQ6xK7kfXq6bQpo1GWEREGpM3vt3Ck5mnAFB0/Dhe+OEOHps6CKvV4MMPcxk5sowlS/y4554IiotNLF6cSXh4ldNH+RebzUZMTEyFylYraXe73djtdkJCQg6Mfi9evJhly5ZhNpvp37//gWnt9Z2SdhERkYMVJH+Nsf0OknYPouP42YSF6Q81EZHG5NdFDq5eOxCzycrwrX+w8NtOAEydmseFF9oPlDMMcDrB399XkTY+lUnaqzU93mq1EhoaetC14cOHM3z48OpUKyIiIvXAnJRl3JkCfb3JfB+qhF1EpLGJjQyFV3bi9fiz0BWEn5/BU08VHJSwA5hMSth9qVq7x99yyy2sXLnyiPdXrVrFLbfcUp0mRERExEecjhKizBDoCeFf28mIiEgjERbmBUckuIJo2dLDzJnZjB9f6uuw5H9UK2nPysrC4XAc8b7D4SArSxvXiIiINETnBbUjpwNcUzja16GIiEgtaNPGw0knOTjrLDtz5mTRt6+WC9dH1Zoefyy7du0iODi4NpsQERGR2lKWD1ZwGlG+jkRERGqB2QyffJLr6zDkGCqdtP/000/89NNPB77/6KOP+PLLLw8pV1paSklJCccff3z1IhQRERGfMLnywQouU4SvQxEREWmyKp20h4WFHTgDPSsri6ioKCIjIw8qYzKZ8Pf3JyEhgdNPP71mIhUREZE69UJuEnneAFr65fs6FBERkSar0kn78ccff2D0fNKkSZx//vn07NmzxgMTERER3/q2IIAMm4NTnF19HYqIiEiTVa017Y8++mhNxSEiIiL1TKfs28jYuI92Q7v5OhQREZEmq1JJ++bNm6vUSPfu3av0nIiIiPhOeNpY+DOQdmfmAzoCSERExBcqlbRPmjSpSo189dVXVXpORESk0TC85f9rKj9ttcxVQnBZKu6Qejr13FvG1NMHcXefaLZHfgbooHYRERFfqFTSrunwIiIiVROQPYfQ5CkUtbuT/KjTOe6LAYywFfLMCS8SEH+xr8M7hKcsC2fIRjqFmkj38wN0dq+IiIgvVCpp1zR3ERGRKjAMvlj1OK3dqZwYvZm/ysLJKitkiQeKtjxISGRf3MGdfR3lQfYV7mBwCgSZDKYP8HU0IiIiTZe5pirKy8tj9+7dOByOmqpSRESkUbBnzuOBvamcuw9+NvVgROsRzB41g6WJA+htcxC1cQImd6GvwzyIw5FFlBkisRIR4fV1OCIiIk1WtZP2FStWcPvtt3PDDTdw3333sXPnTgAKCwu59957Wb58ebWDFBERaciC097h5nDoQQuuOPk6rr46kr3LTsLV6QPc/nFY7UmEbrn9n3Xv9UAXv3ByOsB0o5+SdhERER+qVtK+cuVKnn/+eUJDQ7nwwgsPuhcWFkZUVBQLFy6sThMiIiINmrVoI7FFf/JUMzMl0/7E5TTz66+B3HxzJP2Gdmep9QMW2K30X/ULGVuf9HW4B5QVFQCQVxJNWJjh42hERESarmol7d988w3du3fniSee4PTTTz/kfufOnUlOTq5OEyIiIg1aSOqbACzafQG7szpw2ml2brutiLZt3RQUmLnwhpN4sKgN213w/Lq3sBWu8XHE5ZzF5Ul7sTMSc40tphMREZHKqtbbcEpKCkOGDDni/fDwcAoL69caPRERkbr09LY5bHXCgx/9B5PJ4MEHi7j33iJ+/TWLTp1cZOy34pg5j+tbduHFATfjCu3l65ABmL1/MxelBjDPlOvrUERERJq0aiXt/v7+R914LiMjg5CQkOo0ISIi0qBNzfPSfbeJjYVhnHGGg06d3ACEhBi8+24uoaFe1v7enrKlK/F0fBBMljqP0ezMBsMDwOK9i9mVu5G5hc2Y7nDwbZZOjhEREfGlaiXtPXr0YNGiRXg8nkPu5efn89tvv9G7d+/qNCEiItJgeQ0vXcIHYOzvQ1FBPLfcUnzQ/Y4dPbz6ah4AH34YwldfBQIwa/vXrN34NBi1v5bcUryD6JVnEr7jIV5a8TTjfxrPC7+eQzfvSPj1WVqVHrr8TUREROpOtZL28ePHk5ubywMPPMDcuXMBWLt2LV9++SV33XUXAGPHjq1+lCIiIg2Q2WSm5+pf4K3VnDAgmD59XIeUOfXUMu6+u3wp2QMPRDB13kxuWXQH1yx/jeLk12s1PpOnlGd+PYsH9qbjl/s7o9ueRKDJRHtLGWdZV8OSe2hvOr5WYxAREZGjMxlG9T7GT01N5cMPP2Tjxo0HXe/evTsTJkwgPj6+WgHWlaysLFyuQ/+YEhERqSqTu5BbLkllR0oL7n+mFSNGlB22nNcLEyZE8uuvgcS2KSDq1i6c75/Bo9FW8vp+gyt8QK3Et3vnmwxb8AQAXx7/FW88cQ5DOn7A0yfdBMCmtO58k/4G197bsVbaFxERaapsNhsxMTEVKlvtpP1vxcXF7N+/H8MwaNGiBWFhYTVRbZ1R0i4iIjXNlr+CmLXnsWN/R/JP+J1WrY583nlhoYlRo5qxa5eNwcPy+e3hywjO+RGPXyxZA+bg9avYG3tl+K27ml/3/Mpq83F8/eTvpKZaMZm8pL3bD7t5HUFm+HLnHC6+oWeNty0iItKUVSZpt1a1EZfLxe+//866devIyMjAbrcTGBhIbGwsffr04fjjj8dqrXL1IiIiDd6StBU8sgfaOPN4Ifzon5GHhRm8/34eZ5/djGV/RnDfrHd46YwTsZXuICj9M4rb3V6jsZncxUQXLOaSMHjj6WmkploJDfVSVGTmpref5dszy9ey3x+QXaPtioiISOVUaU17SkoKd9xxB2+99RZ//fUXGRkZOJ1OMjIyWLp0KW+88QZ33nknaWlpNR2viIhIg5FesJ/1Tkj2ugkOPvbEto4d3bz8cj4A095qyXMpfThtL7y0/acajy0gZx4mr4OdGZ34Y2Nf+vRxsmhRJp07u/h25QgArIaZ8OBBNd62iIiIVFylh8IdDgdTpkyhsLCQ8ePHM3z4cKKiog7cz83NZdGiRcycOZMpU6bw3HPPERAQUKNBi4iINAR9bLH8Egcrd/bBZKrYM2ec4eD224uYOjWUb5eGsrwr+OXvZWINxzZzy/v4FcK2ZeeSkOBh+vQcgoIMHnywkKuuioapSbj9C2n1XBRw+LX4IiIiUvsqnbQvWLCA7OxsHnnkEXr06HHI/aioKMaMGUOnTp144oknWLhwIWeccUaNBCsiItKQRLi8DAmGXGdCpZ67664i1q+3kb7pHB5L/I2hrRNrNC7DMHhyfza7iyAmpQ1XXVBKUFD5TIBTTiljyJAyli5tD0BkZFaNti0iIiKVU+np8atXr6Z3796HTdj/LTExkV69erFq1aoqByciItKQecvKj3Ir84ZX6jmzGV57LQ9H0dk89tBuflvxeY3G5fQ6OaPVWNjXl6xVV3POOfYD90wm+O9/Cw98Hx195M3zREREpPZVOmlPSUmhe/fuFSqbmJhISkpKpYMSERFpDDaXJDOnBPZVYQeZ8HCDceNKAdi1q2Y3dvW3+NNu90Pw1mp6dvEjIcFz0P2+fV1MnpzPnXcW0bat5wi1iIiISF2o9F8BxcXFREREVKhseHg4xcXFlW1CRESkUXg5u4Q/3TDcY+eKKjzfpo0bInexwb6BzOJWNA9pVe2YzGWZRK8dS6us87BanuaccxyHLXfVVaXVbktERESqr9Kf/bvd7gof5WaxWHC73ZUOSkREpDHw2gfAvr64y4ZU6fk2bTx0uP4Etg0cw6otL9RITL8vv54F2bvoGPYHbo+N0aPtx35IREREfKZK8+0yMzNJSkqqUDkREZGmqnfOYyx963l6Xl8MFB6z/P9q29ZD7LwYgsP2YSnLrXY8JemzuWnHcrI90H7+hfTr56R1a01/FxERqc+qlLR/9dVXfPXVVzUdi4iISKPS3Lycvu0iiAqv2rT2yEgvQ3Zdw3MDbyfL5oerGrGYPKU0S5rMpaEwK6cZyX/eylWPaJRdRESkvqt00n7jjTfWRhwiIiKNzp39L2bSsGzeSlsCtK308yYTlJjaAGAuTa1WLKHJzxPiTufpFvG88+BaTF4bo0ZVf/ReREREalelk/YRI0bUQhgiIiKNjGFwZmY2fmY4N7CsytV4g1oDEOTdQ1VrsRRuIDjtXQDunf4apaXRnH22ndhYHecmIiJS31XhEBoRERE5Fo+7iKVlsMgOQSGVO6f937wREZy+F/qlFuApy6lSHc+veo4r93v5veAsXpt5LqGhXh5/vKDKMYmIiEjdqdmDX0VERAQAs7uQX+Igx23CPyIWMKpUT+tWUXxcCmVARu4a4lqeUqnns+3ZvJr8B2Ueg1+mnw/Agw8WapRdRESkgdBIu4iISC2weUs4LRhONUcRGWGpcj1t2xhckzuCKZ7jiQlsVunnmwU2Y8bZ39A27Q4y/7qGgQPLuOwyncEuIiLSUGikXUREap3H48RiAsx+vg6lzphc5Ue85ZdEEBZW9VHttm09XHnlAkJDvVx6/f6KP2gYhCY/gz1mNGnLhrDn3bOx2QyefbYAsz6yFxERaTD0ti0iIrVq/d7fOPXz7mxf8x9fh1Kn9uamMqcElpf4ER5etanxAPHxbgCKiszk55sq/FzR3i9xJL1Gs9XnMnVK+WFxt95aTOfO7irHIiIiInVPSbuIiNSqdze8yTaHnUlbvicg6ydfh1NnFmQVcmY63JXnJDCw6kl7YCDEtM2Ajj/x/bpfKvSMyV3If//8L932wKPbz2bb7hZ06uTilluKqhyHiIiI+IaSdhERqTVBez/kufheXN+8FTNbQsTWu7DYU3wdVp1weNrDvr7kpQ3HVPEB8sNKPPFNuOxsvkm5oWIPbH+cHY4y8r0w5Z17AXj22QL8/asXh4iIiNQ9Je0iIlJrvln/DkuT3sR/6f0EhvfD7CkkYuttYFR95Lmh6GY9Fd5aTdzKt6tdVzNbIj39oIefC4yjr4+3FawkNvNLVraBkeufw7V3EJddVsKgQc5qxyEiIiJ1T0m7iIjUDsNgSkYKl2bA99v8eeinTzDMAWzPXI49c56vo6t1nvxk+rZbTdvYzGrX1SZ8JGtam/kw1ovZeZT6vC4itt+HCYM12Zcwb+bdtGjh4b//Lax2DCIiIuIbStpFRKRWmF15nBzkZXgg7Ns5khfe6s4V6Qn0SoHP1zzu6/BqXRfvq6ye3J9LB1V/pL1VvInUnNYAWB2pRyy3Z/dnfLlvK07COeexqQA8+WQBYWGNf2aDiIhIY6WkXUREaoXFnsQbzeGTgNZE+rcHYNFPV2IG9pVmY3IX+zbAWjYtbzkj0mCl355q19W2rYekzASAo+4JsKSkmCsy4Lhlg8nIj+H00+2ceaaj2u2LiIiI7yhpFxGRWmG1JwGwfX9nHnmkgEGDykhdeDvnrP2S/563EcMa4uMIa9d2Vw6L7FBkrfoZ7X9r3drNq44Cuu6Gj7Z/f8RyHmsE4cSzZkcXQkK8TJ5cUO1N8ERERMS3lLSLiEjtKN4JwPZ9nenc2c3rr+cR5G9l9uyLWbQ4yMfB1b5bg5vxVSx0Mfevdl2xsV4ynX5sc8G2vL1HLHdq1BW4n9sDP7/CAw8U0rJl9T8wEBEREd9S0i4iIrXi7o0/0mwXzDAyadvWTcuWXi65pBSAadNCyC/NIDV1lo+jPDyTu4g9S8/DnfRylesY7O/molBo4d+52vFYLNB633geKTiRUaEXHLFc6tLfGNxuAQP65HLFFaXVbldERER8T0m7iIjUipX21uR4YWfm8QQGll+77rpirFaDnc5PGPx5f6767RaCNt2CuWy/b4P9H5u2v8HQjSu4asmzBGR+V6U6Aq0FAFgCQ2skpoyc63j8pYVsy7zjiGVOCr6N3x48hZMH78Csd3gREZFGQW/pIiJSK8bxGby5hnjXPyPDrVp5Oe88O5nbzsTttdLNDyKyZtF8+XBCUqaBt8yHEf9j9b4/AAgwgaPZGZWvwDBY6srlLzuYgwJrJKY2bTwApKRYD9+ku4Qz9+dw8T7wRjT+5QciIiJNhZJ2ERGpFWlJEbC/D11bRx50/aabiqGoFY43V3JN1w9xhvXD7CnBsvMpFs8djF/2XN8E/C93hJWytz08MexJMPtV+nmP18VZ+z0MSQNXcM281UbG5kOXb9no/Pyw93MLNvKHA2YUQ4sWzWukTREREfE9Je0iIlLjbIXrGBs3kRtPeZ0OHdwH3evSxc2ppzogoxcffnghWX2+Ja/rVJ4uDGJ8Sha3zbsK/9xFPoocTO5irCXbiLPCK1PH43TCin3LuGbmUDzZCypUh8PrwpbTG/LaERMRUyNxhUavgfHnsbLlDWAceu56uKeAb1rCw7aWtIm31EibIiIi4ntK2kVEpMZtSP2BP8K+Iq7/xyQkeA65f/PN5We0f/NNEOee15w/0y/B3PYWgsxWRjfvRFnkCf8UPkyCWptsRWsx4WV3Vls+np7AjbcGcePcq/glZw9vLr4Ga8m2Y9YRbAsm+JPV8HIyzSJtNRJXTHhbBvrD0AAw3EWH3A9153B+CAwu6E2rVof2uYiIiDRMStpFRKTG/b5vBY/kwmxXAQkJ7kPuDxzo5LHHCggK8rJqlR+jR8ew6f1HmXnSak4YMQ9M5W9Pi/bM4Ys5g7EU76iz2Bfsms7EDHhjTxsA5vwQQdetn3B+RCQPRjiJWn/ZMTfOM5yFdIhcQ3xUKmFhNXPsWqtmLVnYIpCfWoHVnX/IfVf+PgBSstvQsqWSdhERkcZCSbuIiNS4boaLq8MgInMQcXGHTyAnTizh998zueii8qPJZs4M4rxTevDiS5GUlppwepw8+sc93J22l88WjMJSuqtOYv/B7s+7hfBBalsmTCjf7X7BR+ew7bUtZOR0xlqWTs4313D6SYEcf3zzg77uuiscwwBvxipWPjmA2XeeV2NJe1SUl+yiZgCYXXmH3F+zfxN/2WGvMxp//xppUkREROoBJe0iIlLjzrAW8n4LiM++FMtRllfHxnp56aV8fv45i8GDy3A4zLz4YignnNCc2TNDGd/9GvoFBnBlUDHN1l6Exb671mM/Pup8+OM+slZO4J57inj11TwsFoNNO1twyuSfyShozuqQdZx//smkphgkJ1sPfH35ZTC7dllYmLaCEWnwfFk2AQE1E9e/k3bKcg+5/1h6CUPS4AfzoTMbREREpOFS0i4iIjXL6yKYlPL/G9y+Qo/06uXim29yeOutXFq3drN/v4U7bo/mu4cmM6nTX4SFdsbi3E/02gsxuUtqM3pMe0bAvGfoFjiM0FCDc85xsGxZBrNnZ/PSO2F8GPgfrsiA5wLWMOeDC5g9O5vZs7Pp2dMJwKpVfqQU7mWRHXZQc0fYRUR4edSRRbfd8FPSH4fcd9nbQ34brJ7BNdamiIiI+J6SdhERqVHe0mRKvG5KywKJaFnxo8dMJhg1ysHChZk88EAhwcFe1q7149yxvfhs/2zcthhyStJx5Syutdj9c36jc8G9nN5rDv37Ow9cb9nSy8CBTgYOdDL+lMs5pUVPbom00jvxzAPXhw37J2kfYG3BV7FwVlnvGovNZoN9hpetLkgr2HfI/f7p02DqHvqFnF5jbYqIiIjvKWkXEZGj87qxFm2s8C7uWzKXE54EfVMN2idUfj13QADccksxf/yRyejRdrxeE9fe0pUTktzEJsP8tIWVrrOiduz5mqio9zi5568HJe3/ZjVbeXfU99w5ehXOFuceuD5gQHn51av9iDPMXBQKnV3dajS+vnvH8XDBCPoGnnbQdZO7iO7+nzKy+3ztHC8iItLIKGkXEZGjCk7/iOarTid80/UVKp9EOBgm9qT2PezO8RXVvLmX11/PY+zYUjweE+6cKExAXuneKtd5LJN3LKJnCnzqyjuQhB+OzWzD61e+vtwwDD5b/xqDO/8GwNatVlyl5UeyOQmv0fhWrXuaJ15aQEbpuIOuW0uTuO24G/nkxsuVtIuIiDQyVl8HICIi9Ztf/hKmF8GTe35kdugH+Le9+qjlT2l5DjwzjrLgLDrcVr0E0myGF1/Mx+OBFZ/NpKvNzPCvQ6pV55GY3CUEeosIMkHR7nNo375isf938a18tH0WK0OtnDygA7+t7Mfa7DRcflBgPsoufFUQFVU+cyE39+DP3FelL2JSKrRzGFzVSUm7iIhIY6KRdhEROSo7Vu7PgfVOeHvj28ecJp+cbIGycCK8CURGVv+4M4sFpk7NJyikG1v2JLJ+U1i16zwcv8JVfN0SNka0IiHsNEymij13VocLCDKbOTPIzZfXj6ZVVBqP7s/juFSYT0GNxhjUPB26zGZdwc8HXd+Zu5W/HLDZ6aVVK+0eLyIi0pgoaRcRkaOy2O1Mj4W7Ylpy4+kLOFo2aytYSd99JzL5ogfp0MFd4cT3WKxW6NixPBndu7dmR68BMAxCdr8IwLwNZzJwQMVHq4+PH8myixZxaWxnmgWl89M9Z5GxrxvktcNrDKjRMKPbfQDjx7ApYMJB14eYQ/mmJQzNOZ7IyIrtPSAiIiINg5J2ERE5qhd37WJmMWT9eTde79EPHf991xfclLmVrDbz6NPnyGvCqyK2TTLDbhrGKvOgCm+KV1FFaZ+xPWsFpWVBTJr56BE3oTuSqNAEcnt9gsPUnE7xG3ih61Zsr22jb+DZNRpnjH9rBvpDd7+Drzcvy+f8EIgrHlZjH5SIiIhI/aCkXUREjurTQieT8+D3PVZeeCEUw1PG9IUXYt/18iFlF+5dwtfFML/ESt++rhqNIybWxJ/NlzDbtR2X49Ajz6pj8rZ59EmB85aewP6CVvTpU/nYPQHxJHd9gxNTzdzvv5JHbhlFeFjNfnDRIWwIy9vAey08B31wYXWmA1BmaVWj7YmIiIjvaSM6ERE5Kv+U22F/CsnbTmPakhDyu1zApzlL+HHvUj6OOg5v5ODygoaXywLyaBENn/xyGf0ur+GENa451+YG0yWkBBxpEBhXI/V6DS/5pgC8wIK599Gtm4ugoKqN5AfF9Ccztw/+Yas5occ8kr25QM1tnBcQHgmAn8WJyVOCYS2v+8/SJOL9wBXUrMbaEhERkfpBI+0iInJUzXb+B35+lVhrJwzDxNy3niXcYuOcYINmW27C7MwFwFq6i0G2Iu4IDiRvxzW0aVOzu5jHx3uZUJbI3ZEQ6s6psXrNJjNP9X2L0I834N49kvHjS6tcl81s41T797Sd/QVTpv2Af1h0jcUJEB4VgN1ZvkTB7MoDyo+cuyTDzpA0KAq31Wh7IiIi4ntK2kVE5MgMA4s7B6vFxUMPFdKmjZuMjX04Y8tGbmiZgNW5n4gtt4Lhxa9gBQDLdg2mV5+j7ldXJXFxHlJzWpd/U5peI3UGZH5PcMobfPr6foqSEunWzcVll1U9aQcY0i+AtavH8fO6swgLq/7u+f8WFeVlVDp03Q3JuesBcHgcBBb0h/w2tI/tUKPtiYiIiO8paRcRkSNzF/HDTa1wfuRHq9hiXnklH7PZ4KvPOzMz6yO85gAsuQtx7nyGtak/s8QOi3YMpm/fmp0aDxAd7SU1P54MN6Rm7KyROlOS3sWy80lKk34H4PHHC7BWc+HYvzexq42kfafLYJsLMgpTAQi0BhLz/UKYuof28X5HfV5EREQaHiXtIiJyRHklqYTsAv+dEBBiYeBAJ7fcUgzA9fcNZVnYA4xMgyv+msaTySsYlgZfuQro16/mk3azGb4L30RsMjy/e3H1K/Q6uXTbKiJ3wXe5wZx9tp2hQ6sfd0yMl/PPL2XQoDLatq3ZJQLh4V7OSDmXRwpGEO/XGwBr4UZOavcxfdutplWrmm1PREREfE9Ju4iIHFGJPQMAP5OJyIjy89HvvLOI3r2d5OebuXXqBax3WVnnhAyioKQZW5ddU6Xd1ysiyNMOE1DqLqx2Xa685ZR6DdxAxs7RPPJI9ev826uv5jNrVk61R+3/l8UCX8/8gsdfWkCR5wQATCkzefuaa7l6+AfExippFxERaWyUtIuIyBHFW0Mo6gBzg9oQGlo+1dtmg1deySMgwMuquT043/Mxv47+irujV8FzmXQM7k1YWM2eo/636IIHSXhrI91yNle7rsjiVSS3hzfzz2LcucHExzeMhDcqqvzfITe3/C383i3fMiQVfvOY8Pf3ZWQiIiJSG+pl0j5nzhxuvvlmLr30Uh588EF27qzY2sU///yTiy66iGeffbaWIxQRaRqcJSWEmMGvLJrQ0H8S8Y4dPTz6aPnI9JdPj6M4ZwSrV/sBJvr3c9daPDHNo9m1rwd70qp/jJp//lIA1m86ixNPdFS7vroS3GoXpq6zWLv/LzAMVpVm8pcDcj2tfR2aiIiI1IJ6l7QvWbKEjz/+mLFjxzJlyhTatm3L5MmTKSgoOOpzmZmZfPLJJ3Tr1q2OIhURafzKisvXr5c4w7BYDr53+eWlnHyyg7IyE7feGsmyZeWboNXGJnR/i4srHw1PT7cco+QxeMuw5Zfvdr9464kMHlx7Mde0tsfdgzHufDaW3IylLJ0ZLb18GmMm3D3G16GJiIhILah3SfsPP/zAySefzMiRI4mPj2fixIn4+fmxYMGCIz7j9Xp59dVXueiii2jevHkdRisi0rityd7Ewznwq6f4kHsmE7zwQj7R0R62bLGxbFn53Oza2ITub61aeTjumrNxDu1G2v7fqlxPXu4qEpPLuCYtAFNkB8LDa2c6f22IMcczyB/amMFavInOfpBYkEivznr/ExERaYzqVdLudrtJSkqiZ8+eB66ZzWZ69uzJ9u3bj/jcjBkzCAsL46STTjpmGy6Xi9LS0gNfdru9RmIXEWmMlhaU8GQufFt2+EQ8JsbL88/nH/g+MNBLly61Nz2+VSsPma0X8Yd1N3uy/jrkfkDWT4QmPX3Mev4ozGGLy+DztLYMG9ow1rL/ra/lNJa1gbtCw7AVbwJg7Z4+JCbWzuZ/IiIi4ls1vK9t9RQWFuL1eomIiDjoekREBOnp6Yd9ZuvWrcyfP7/C69hnzZrFjBkzDnzfvn17pkyZUuWYRUQaM3/jXFgeSJFfzyOWOe20Mi69tITPPgumXz9Xje+Y/m9xcR6GFyZyY4dltPUeegZ6SOqbOKJPw1qyA3dwJ7JyVtK2ZAWOFhfg9f9nJHp4q+HEzJ9BVpaJoQ+U1V7AtcASFAlAkCWHn5LnYxTDH+nt+Y+SdhERkUapXiXtlWW323n11Ve5/vrrCQsLq9AzY8aMYdSoUQe+N5lMtRWeiEiDF+c8AX46h4SRDiD3iOUmTSqgSxc3J55YuwlwUJBBj+yTuHvAMtIc/9OWx8EV29fQzrqKy08eRlBAGSO+u4jW5jJ+iJtM8xYjKY0diyP6NIqzI8lafAEWi8GgQftrNeaa5hdSnrSH+uXw5L5WbCqB9q5QJkUf+iGGiIiINHz1KmkPCwvDbDaTn59/0PX8/PxDRt8BMjIyyMrKOmik3DDK1yWOGzeOqVOnEhsbe9AzNpsNm81W47GLiDRGjmI7VkvIQTvHH05gIEyYUFInMZUarQAwSvYedD0vazFfFXkxAaPtHUlybqTU6yHXZKOV1YUldz4BufNJckFvKzx6/qN8l/zgMV9bfeMNNTMsFXI9TmLc/SHVRgvbsZeHiYiISMNUr5J2q9VKQkICGzduZNCgQUD5JnMbN27kjDPOOKR8XFwczz///EHXvvzySxwOB1dddRXNmjWrk7hFRBqrMyIncN9Hc3hvw2tA/did3GFrSYYbUsqSaf+v62Glm3m/OfyU0oUTj+vMiBFteer8rbTrv5ns6AiC9s8gMOMb+u7aS08/uCBuDUNbNqyp8QDNoiJZklr+/4elXMHv7w/luNuLgCKfxiUiIiK1o14l7QCjRo1i2rRpJCQk0LFjR3766SfKysoYMWIEAK+99hpRUVFccskl+Pn50aZNm4OeDw4OBjjkuoiIVN5/CpaxNBuu9FvPWfUkaU8LzyI2GTpYd7H4X9eblWzg6nDYtP5aPB4Tv/0WwG+/dSAsrD2jRtkZO7YbCT2vIWjnSP505LF+RyfevKLhHPX2t+bNrNxVeDyndVqO3fki27p4SEzs4euwREREpJbUu6R96NChFBYW8vXXX5Ofn0+7du148MEHD0yPz87O1jp0EZE6UmKU4Qb8rBXbN6QuxIQlYgIwecFdAtZgMAyseeXnri9PGsIvv2Ty44+BfPNNIHv3Wvn882A+/zyYNm0iOOu0JN6fmUVpUXsGvprl09dSFVFRXl548Xc6Xn8Z15/wGau39iQxsbOvwxIREZFaYjL+XgTexGVlZeFyaeddEZF/K/12KIHBe/hh/7eMvmyAr8MBYNZsP156MpNW7eP4YnopAK6iraxceDL9rTYufjOHWd+WTxX3euGvv/z45ptAfvghkOLif046HTDAybffZvvkNVSH1wvt2rVk2CMRbDYX0nXNvXz5ym3o82wREZGGw2azERMTU6Gy9W6kXURE6o8Y/2LCrRAUVH/2CGkdb7BrX3fKLG6gPGlfn7eLsfshzGPlor7/lDWbYehQJ0OHOnnyyUJ++SWAb74JZPlyPy67rG42zqtpZjNEdlzDYqMQPGAy91bCLiIi0ogpaRcRkcMzDIJsBQD4B4f6OJh/tGrlAWDfPgv5BUlEhLWlxByKf14vCvd1YvCph581FRhocN55ds47z16X4daKAZeczRwDLgyB0KhTALevQxIREZFaoqRdREQOy+sp5Yl8N2FmaNOs/hyV2by5F0tgEV0njGTYjFWsOPVpeodfifOViwGDgQ9m+jrEWtfD30secEYQlCTaUNIuIiLSeJmPXURERJqiEmchT+TCPdkQFBbg63AOsFggNtqf7KB08r2weMNk0lf/SaBfKe3aeomJ8fo6xFrnt+19FsX6sXrWqyQmaj8WERGRxkxJu4iIHJZhDca6+kZYewXREYG+DucgPbp7yJ/+Ob81j+TKoGJO4kJWPdmfAQMa3hFuVZHiHEHohCLe+/1mOnTQKLuIiEhjpqRdREQOK8gchvu712H2R4SG1q/R6//8p5iy5BHMmj2JT4ug9W54KMvCoEFNI2mPjPTi8vjRrZsLqxa6iYiINGpK2kVE5LCKCj1YzOWjuKGh9et00L59XZx1lp2351/HVieEm2FXTisGDmwaSXuXLuX/LsOGlfk4EhEREaltStpFROSwLPvm4PrYxvyHTsZWf/ahO+C++wrxGH589dnb7CkJZvey++nYsWlMFR8zxs4vv2Ry111Fvg5FREREapmSdhEROazv9v6O3054yLzR16EcVseOHsaNKyV55dWUvLqV/i2GYm4i72omEyQmuvHz83UkIiIiUtuayJ83IiJSWYWOfNyA16iHw+z/7447igjwM0NhfJNZzy4iIiJNi5J2ERE5rFEBnUhtB1eXDfd1KEfUsqWXxx8vpE8fJ+efX+rrcERERERqnPacFRGRw/L32IkPhBCjpa9DOapLLy3l0kuVsIuIiEjjpJF2ERE5LJO7GAAXYT6ORERERKTp0ki7iIgc1vzSrfzmhlJboa9DEREREWmylLSLiMhhfVlUwAoDLsLu61BEREREmiwl7SIicljezAmwbz3mjuf4OhQRERGRJktr2kVE5LBapd4KMz+lW/AQX4ciIiIi0mQpaRcRkcMqKfYCEBrq9XEkIiIiIk2XknYRETms7y5vQfF7wcSFp/g6FBEREZEmS0m7iIgcwvA4aZ5SQrv0Ukr9i30djoiIiEiTpaS9KTO8lK66jLVzOpP+x8m+jkZE6hGHM5sSA7I9EBEe4+twRERERJos7R7fhLnzV3Hc2gXkeOGKyL087euARKTeCDKcpLSDDIc/jogwQOvaRURERHxBI+1NWEThUta2gdPCI4mOH3vgurks04dRiUh9YHIV09oGrdyRhIX5OhoRERGRpktJexPmn7uQeBtcmvMI6V+/xp23F/PtN8fx3PcDMXlKfR2eiPhQWVH5OvYCe7h2jxcRERHxIU2Pb6JM7kL8ClYB8Nhb55CcFQTBYUzvkYoXGL/7M+I6TPRtkCLiMzuydzEjDzx2D5f5+zoaERERkaZLI+1NVEbat1yX4WZaShxGcBseeKCQQT0i6Zvbh/9GQlT+b74OUUR8aFVuNndnwzt2zboRERER8SWNtDdRczM38W4hJNhhzBg7t9xSTOfOLp596DOefLYHRvFf7HflYdgifR2qiPiAlxNhfRrZnk6+DkVERESkSdNIexPVvc2l+K++naS5jzFihAOAESPK2FfSlXV7emEyXARm/ezjKEXEV+I8g2HmZ7Td/aCvQxERERFp0pS0N1Hu1H6UffcSEUkT6NvXBYCfH5x9tp3PloxntQN+3/G+j6MUEV8pKjIBEBpq+DgSERERkaZNSXsTZLGnsGRReaI+fHgZFss/98aMsfNBRjT9U+E/yVvBrfWsIk3REOs9FL0XwlWDX/R1KCIiIiJNmpL2Jmjz6pu5rEcbLh7y+YGp8X8bPNiJZd84/L1WTg6+AK8lwEdRiogvPVMwn/bpJSwKWOHrUERERESaNCXtTY3Hwd271tArzcOf1nRGjCg76LbZDOedbaLsgwXsn/chZtP//4i4ijC5i3wQsIj4Qo63iGwPGKZQX4ciIiIi0qQpaW9iPIXrCDQZhJhMBJdcTIsW3kPKjBljh9Tj+W1eEOvX27BlfMd1M3qSuukRH0QsIr7wQngM69tAb8tJvg5FREREpElT0t7EhNp38kdrmGE/mVOHRhy2TGKii759nTgcJs49txn3/rqCH4tdTNo0A4tjb90GLCI+0cavgJ7+EB7YztehiIiIiDRpStqbGEvRFgDW7+nDyJFlhy1jMsFnn+Vwxhl2nE4THz57H+3MgVwe6iUk6dm6DFdEfMEwCPHLBcA/LNLHwYiIiIg0bUramxhPTnnSvnV/T/r3dx6xXHi4wbvv5vHoowVYCzsR/eECxoVCUOY3WIu31FW4IuIDXlcRrxSV8UEBWMOCfB2OiIiISJOmpL0pMQzGJK3g5DRIDw/EZjt6cZMJrruuhEmTCliVNJjZq8diwiAs6am6iVdEfCK/LJN7s+GaTIiIUtIuIiIi4ktK2psQp7uE3+0w3w5h0V0q/Nyll5aSkODm7k8n812RmfEb52PKWVyLkYqIL3ksEZg3XAabLyCmmcXX4YiIiIg0aUramxCLJZDeKxbD7A/o16l1hZ+z2eC//y1kV24rLtvrz+wS+HLze7UYqYj4UqA3Bu83n8DXM4iKOvSECRERERGpO0ramxCL2ULKssGw9ioSE92Vevb00x0M7GOF+U8wuPgSTh/0ci1FKSK+lptjAiAgwEtgoOHjaERERESaNiXtTUhJ8l+0DNyEn81J166VS9pNJnj44UKK/ryL5S98yr6kZrUUpYj4Wui+dyl+L5g3rrnF16GIiIiINHlK2puQBeuv54mHenP+Kd9XafSsf38XZ59txzBMPPVUGBb7HvxyFtVCpNIoeV2YPHZfRyEV8N7eX2mXXsrM8L98HYqIiIhIk6ekvalwFfFQTjbn7YOCZv5VruaBBwqxWg0K837grm+HMWnhNZjcJTUYqDRWwekfEbN8OAFZc3wdihxDjjOXbA+4qPrvChERERGpGUramwhv0UbODIJEi5Ue8QOrXE/79h6uuKKE1QXhfFZk8Faug5JdL9VgpNIYmVy5PLnsKT7OTGfp4gJe+Xgjn3x9Cb/+6MTQkul65/qg1mxoAye6T/F1KCIiIiJNnpL2JiLEvoOPYuHZolPo3d1arbpuv72Y4KyT6bbzApa2hoTMjzE7s2soUmmMdm34L8/nljEhE0a/E8PLjjN5pHAR2duu5pNPAn0dnvyPGIpJ9IdIawdfhyIiIiLS5ClpbyJMBVsAWLenNz16uKpVV3S0l5tvLmbLp9OxZA7A7CkhZM/UGohSGiNryU4G5P/A882g1Y5ziUwfR1zJEIYFmLii10KKV75HXp7J12HKvwSYcwEwBUT6OBIRERERUdLeRNizNgKQlNuT5s2rf+7ytdeWEBvr5Y5PngXAm/Yx5pKkatcrjU9o8tMEmL102T2avZ/N5onHi5hzywd8NfwJmlvhsXPv57v31vs6TPmXGY5U3i8Ae2D1ZuWIiIiISPUpaW8KDC8nJ6+hbTLsjTYw1cCgZmCgwb33FrJw80hu3dyd9kke/lpzd/UrlsbFMPDPLT9h4OHpkxg4sIxzz7UTbAvG1foq9pguwGrxcHq7K9i1MdfHwcrfHsl3MCETHMHacEBERETE15S0NwFlbjtbnWZS3BAT3a/G6h071k63bi6+3DqAXC98mJOHdhWTf3PbU7kjw87LuSY2pHdh0qTCfz40MpmwDXua+9NjGJqfyZZVl2F43T6NV8AwDEy7LoYdZ9IiuquvwxERERFp8pS0NwH+tmB6/7oX3l9M/y6xNVavxQIPPVRI9k8vEfTLa9zdez5/Z2Qml0ZNBXY7SnglH+7K8Of8MQa9ex+8n4JhCWJTaH/KDFhGMovm5PskTvmHyWTC9uOH8NlPtIkJ83U4IiIiIk2ekvYmwOuF7etjIOUEEhNrdiTzxBPLOGFAMKVLb+aF58o3rbLnreTHOf2w5q+o0bak4fGzhGH+6y48K6/n1luKD7lvMpl46YxXudJ7DzMnJ3HfpO7Y7T4IVA7wuA3y88vfGqKiqr//hYiIiIhUj5L2JqB083ec1m02sVE5dOhQs0m7yQQPPVSAyWQwe3YQn38exHO//4eJ+138sOGFGm1LGh5TYRu8c57Hb/5LtGvnOWyZIFsQD19xB0Hh4aSlWXnzzRAwGk+yGJD5LTHLR+KfPdfXoVSIJ20+he+E8P3do4iMbDz/DiIiIiINlZL2JuCzbfcz7KoLGDF8LtZa2Aw6MdHNuHGlANxzTwSLdjWjnRXyinfXfGPSsOz6kptOncbQ3ruwWI5cLDDQ4OGHC8Hi4Jeic/lj7kngcdRdnLXEVriayC23YyvdTuSW/2Bx7PV1SMf04+5FtN1byouBq7DZfB2NiIiIiChpb+xcBbySX8Dd2WCPDq+1Zp5+uoCHHy4gKMiL6YsPSG4P/wnMbVQjplJ5wfbXeOXKWxiauOmYZUePdtDlkqfY0HIhN6bugK33H7sBwyAgaw4Bay/HL3teDURcc8zObFb9dQUlHieGyYbJXUjpuhvBOPyMg/oioziDHC8UG0f5lEVERERE6oyS9sauaAN3RMCZtkD6tO1fa83YbHDDDSUsWpRJdNsEHE5/bJRgse+ptTalnjO8nL1/J4E7ISPq2KPmJhO8eOl1NNs/kA+aQ1zWdAL3Tz9ieVvBSkqXncV/fptAixXz+W3dYzUYfPUty9nBOSkFDEzxo+/z0zlvn4UTNq/CuWuqr0M7qjP82rCxDVzkGOHrUEREREQEJe2NXlDpDh6JhpvyTqJXj9r/546L83LJZWVsTEsEwFayudbblPrJU7qHLA+4gNYtelfomT49zZxVuoBVvz4GQPj2+7EWbzmkXHDqO8SsOZfduev5rAjuah7JkOFzajD66rNY/AkyxbBzw5ms23gyC7OiyXTDX/lpvg7tqELcJfTwh2hvB1+HIiIiIiIoaW/0vLnlCc+6Pb3p3t11jNI1o0cPF0/nmui1B2Zs/6ZO2pT6J7AsleIOMC8wgR7toyv83D33FPLqgv8yZ93pFLkcbFx22UFHCAbt+4LwXY8BMKDVRQy138C0qb/w9js1d5xhdZiduWAYhOQPwvnaStwzP6VZWCCFH8+hz+IZ9O821dchHpXFlQOAxxrl40hEREREBJS0N3p7Mtdi98LekkTCwow6abN9ew/J9hA2OGFpVnadtCn1j6k4GbMJSjO706FDxddxR0UZ3HVXMeM/fo4+u62MTt5P7pJTwfBgLt7Cu3/dQ4kX/si5jfaXfM6SKW9QvH0gL74Ywq70TOxFO2vxVR2D103kunGEr7qYh+4swp4Zz5D+Nn77LYuWpl6smH8BjzxSfvZ5UeFWbHlLfRfrEfzm2M77BZDtp/0oREREROoDJe2NmeFhXNpmQnZBSvO624nbYoGybVPg8+/o7PmkztqV+sWeWb6fQXJ2J5o3r1wCePnlpbSI7ER2an+CTRaaxV8MJgsPb/iSe7INhu1qyQn/eZH8fAtdurhISHDT/LSbOf2nfsz447LaeDkVYk9+meFbNvHb3rXs2BVCXJybt97Ko1kzL6++mofJZPDll8F88sUeRs86g6fmXUzYplsI3TWZ4LR3Ccj8HlvBCsxlGT57DdMK85iQCXv8in0Wg4iIiIj8Q0l7I+byekhzROEFWkcPq9O2B7TuAdtHk7qldZ22K/XHB6kLuTMLNtksmEyVe9ZqhScmFVP88a/kfDCXX3bdi2HAqHaj8XNHsW76i9hsJp59Np+5c7O4++5C8rI7YDcMlualYitYUSuv6Wgs9hReWfMyy8vgqqRIskujefvtPKKjyz+wGDLEyX/+U54IP/TpVnY4Xcwq9uDYP4vQ1NcJ3/ko9y64gefmnodn28MH6i3zlGEYdTNLBsCRdilsP4sg/5F11qaIiIiIHFktnNot9YXN4ke7mels3pPDwJf9gbobbU9MLF8/v2mTtfyIK5OOj2pqPsiOIBk4gZZVen7YMCdnnxLMjz+O5MqroW9fJ23anILzl11YXOG8/mYeZ51V/jN99tkOnn72RiZmzee5jj9TtvtFcnt9TqU/LagCs2MvIWnvEbTvM56M8rAvL47pX8zg5uvt9O178D4Sd99dRFGRifffvxqT18oNY7Owdcqj2LkfR2ka7+0o30zvtoB4AEyufD5YdAV7ivfy9Lkr6+T1RK+aQsoaPxJPyaUuf2eIiIiIyOEpaW/EnE7YucMGzpZ0716302179HBx/bhb6TzwI3ZtvoQOPR6p0/bF9+L2X0dy8lDadx9R5TpefDGfli09fPppEGvW+LFmjR8mUyAvvZx/IGGH8pH5Gya6+OCVl3ly4HwC8hYTuP9r7C0vroFXcmSugvXMWXg2ZwR5CbFBfkl3Vk77nvYhbbjjjsxDypvN8PjjhURGennhhct5YD08+5KHuDgvMa0K6N/5Hczh+5i+dhItMzwERZUxJ301G50Gk0t2Yg7pVKuvB8MgL698AlZUlNa0i4iIiNQHJqMu513WY1lZWbhcdbO7el0pXvk2774TxI8bLuTXP/3rYpDuALsdLnjlbNZFreWelt25fdTcumtc6oULL4xmyRJ/Xn45j7Fj7dWqKyvLzJtvhvDzzwHcdlsRF198aH12u4lBg5pz7dDnmXTx/fzsCGTIyb/j9a/aSP9hGQYWxx48ge0AuG7edfyY/CP/adGSU81TOeeGsRiGmW++yea445xHrer994N5/PEwXK6j/Idp8tLt0TAGhZbw8NCnCWxzRc29lsOwlO4i9I/TScpIIHfIb3TsWPENBEVERESk4mw2GzExMRUqq5H2RuzZXc9hPamUnsEtMJlOrdO2AwMhsnAYp7RaS1tPQZ22Lb5nK1rPFYlzaOk+noSEE6tdX0yMl4cfLuThhwuPWCYw0OCaa0p4/tUbmD7wMZINO7+smEjisO+rP63c6yIw81v27niZ9kY2hUNXYFhDuKD9eJbsXstX3z/IKz9dBMCVV5YcM2EHuOaaEsaOLSUtzcK+fRb27y//3337zP//vxbS0y1cu+M67jzrJYrsWymq3qs4pi371zA21U5b004+jtZIu4iIiEh9oKS9sXLmMr24lEIvjItq5ZMQOrtu5a34aXiMDDK8ZWD290kcUvey9i7gjKEvEx20h/bt624TxGuuKeHTT5uTt/kcYhO/xmEKBG8ZWAKqXKfZmU3U+ku5M2kj0wrg9RY2Tt+7kTdmnsIHH15MXu7F4LUSHOzl0ktLuffeI3+w8L/Cwgy6d3fTvbv7sPe9Xnjo8kHl/z9zDXSp8suokLScdHK8EIqJ8HBNwhIRERGpD5S0N1Lmok282RwW5YWRkNAbqPup/y0SWpBbHElUSB7Wkh24QxPrPAbxjQ+2z+P5DBhk2cGsyLpL/sLDDZ5/Pp/LrnmX9n/djfvF9mA59qj3kZgde2m2bhxWexIJAYFQYOf1HZdx5x1jcDjKR+/j4txMmFDAJZeUEhZWs6/VbIbS4P4AOMs2YvKUYliCarSNf+tuimJjG1i8qx9mnS0iIiIiUi/oz7JGKqB0G+ND4YzMkfTyUa6cmOhm7Z4+eA3wFKz1TRA1wevCL385eLSTdkWVOLKxASGuDnXe9siRZVwy1kzynoHccUcEJSUmMIzyr6Pwy/uTiC23Yy4r30DOUppM5MozsNqTsJvjWTlvAby6lY0vvo/DYaJ3byevv57LkiWZ3HBDSY0n7H+L7xZBh10Wmid7yc1aVCtt/M1aWkIPf4h1JtRqOyIiIiJScUraGyl31hYANqT2onNn32yw16OHiyeKSgjbBe+uffWYSVN9lbfzRabMHUPKX+f7OpQG44lwM6UdYVDZBJ+0/8gjhbRq5SYlxcoDLy/isum9ce/97MgPeF0sWXUH2WnTCUl9HYA7/3qaPjtzSXe3p9edf/DFN4Mx5Xbm9NPtzJyZzY8/ZnPuuQ5sttp9LYP6m3G5ggHYkr2hVtvyOvIAsHuja7UdEREREak4TY9vpJbtX0GCFfa7uhNQ9eW81RIZaZCUeiol0ctZ7ejEhLrcvr4GvbvjR6bmQ5uIUtr4OpgGwOzMJtp/NwCBUb6Z5hEaavDCC/mMuySceaE3U1CQy7Tlj3BHzKl4/VscUr44+TUu37MXtwEtXxvPOSeb+DF0PgVu6PbWrRTua8sppzh49NECEhLqdkf1vn2dZN31G+R2oOPSMqD2NohbXrSZVVbYb3YzotZaEREREZHK0Eh7Y2R4uDUjie57ILV5be83fXSd7Xdj+mIWidkzfRpHdYS5iwHIcI04cC1jx8sE7n4ZvFVfL91Y+eeWT+FendyX5m0ifBbHCSc4ufJyJ0Vff8UYSzQPhpcRsf2+Q2Z8mJ1ZmHa/QS8/CC5ox65VJ/LSsy0o/XwGfDmTwiW3c8klJbz3Xm6dJ+xQ/gFE+/De4Ihk1Sq/Wm3ry8L9XJsJKyy5tdqOiIiIiFSckvZGyGV4KcgfiNkVSOcWI30ay5BBJoxt5/HWm6Hk5powO3Pr1zR5w8DkPsoHG4bBAyHFGJ1gxiN3cuutEaTsSWXUH89x6uJnKf3zRPzyfq+7eBuAd7dO5/S98GJSAgkJh98Vva7897+FtPaMYPtbCwk22QjImUtgxsEfIIUlPU0nSwlT3f3IfvMvrrzCTo/uHlybz4KtY7jttmKefbYAqw/nJfXvX/7h0MqVNjBq74OD4uxxsP1sPGVaCiIiIiJSXyhpb4RsZhvh3/yJ96li+neP9GksV11VQseOLrKyLPzw9o+8/eNAHCkf+DSmf8tafwczf+wOKe8c9r7ZmUmApQSP10xSZgIzZwYx8qoy8tz+5HnNtHKl0GzdOCI234S5bH8dR18/fZ4Jv5bCnNRedOrk26Q9ONjgpZfy2by3B4/OeBSA9E3/xVyWAYB/ylsE7f8KgJs/nMaAHhFMnlzAL79kMXNmNl98kc299xZV+5j36urf38kF151FZvsYkna8WWvtxO+5Ez7/gX6hp9ZaGyIiIiJSOUraG6GSEhO7d1vAMNO9u282oftbQAC89FI+ZrPBx9YXeTTbwesb3/VpTP/2wN5Ubsn0cs7Cx/Df88Yh9632JACSM9tz/4N2+vZ14tg+HPtzu7H89CsppmsxMBOU+S0r5g8jMPkFTK6Cun4Z9crArBdgzot0878Yf39fRwODBzu59toSpvxwDxcmNSMxqYiVyydidxYw9s8XmJwLj37zGCuSBjN5cgEmE5hM5c8NH14/lj8MGOBkc/hmZtrLWJo2v9bayc8vf0uIiKi9dfMiIiIiUjn1MmmfM2cON998M5deeikPPvggO3fuPGLZZcuWcf/993PVVVdx+eWXc88997B48eI6jLb+Ma99nDevuZ4Teq0nJsb3f3z36+fixhuL8Wy8mCEBEOepP+tlr2k9DYAzgiA6+UlCdk896P78pDl03Q1357o57zw7332XzUsv5RET1Iwdf55M18ve4Zbvf+frsgTOSXNw+u8vYnIX1v0LqS8Mgw3ze8Ffd3DmkEM3fPOV++4rpG1bM/PXl48gr87bwa9J37KwuITHM/15fN41XHVVCT16+HZmwJG0b+8hNm0Uj0fBEHd+rbXz6YVdSXu1FfHhR/6dKyIiIiJ1q94l7UuWLOHjjz9m7NixTJkyhbZt2zJ58mQKCg4/ehkSEsL555/Pk08+yXPPPcfIkSN5/fXXWbt2bd0GXo/cuvsDpnd+h5Z9lvs6lAPuvLOIoPT/8EcrE3eGFR2YnuxrL07qAc/vo9W2xwAI2/0c/tm/Hri/JC+KbS74K7MtLVp4MZvhoovsLF6cyfXXF2O1Grz+5VAufe0xgghiQEx/vIGtffRqfC989SVMGnEOPVuvZ+TIMl+Hc0BgIEydmkfed28zaNFztPPbwMi4K4nb8CzOj+bRIjCOu+/27aaNR2MyQZz9Vh6OhqG2HZj+f3PEGmUYnJC7hxML0yn0y675+kVERESkSupd0v7DDz9w8sknM3LkSOLj45k4cSJ+fn4sWLDgsOV79OjBoEGDiI+PJzY2lrPOOou2bduydevWOo68fjAcWSy2lzHPDgFRnX0dzgEBAXDN9bB1X1cAbMW1e950Rfhve56hUdNobjZx0+uPMn3N/58pnvXLgTIJzmvgo3kEb5t80LrmsDCDRx4pZN68LIYPd+BefTmlT+3ns//O4bnnQsnONmN2ZoPh+5kOdcXkLuTF3b9jSviV8OZW2rat+53Wj6Z/fxc3XWuwfMHd3HVfKy65JJr0b+4hsmgIn3+eQ3h4Pdog8TBad21JUmZ7zCYPfgU1/4Gc11NKsht2uSAkNKrG6xcRERGRqqlXSbvb7SYpKYmePXseuGY2m+nZsyfbt28/5vOGYbBhwwbS09Pp3r37Ycu4XC5KS0sPfNnt9hqLvz6wlW5mQTxM9o9hYKeuvg7nIL17O1mV3B/DAFfuGt8G4y3jtjWvEHj6XSR02UxMjIcXNjenTTJcvf6HA8Wy98RC8skkRvQ/bDWdOrn5/PNc3nsvl3ZxgRRmNGPqK/5M+6kLV03vTVnxsX9uG4v0tJk8kmtwVjq07xvh63AO6667iujSxUV2toU1a/yIiPDy5Zc5dO1aP6fF/9uAAU7mbhrBX3bYnvpdjddvdhexrg0sioMWEXE1Xr+IiIiIVI0PDzE6VGFhIV6vl4iIiIOuR0REkJ6efsTnSktLuf7663G73ZjNZiZMmECvXr0OW3bWrFnMmDHjwPft27dnypQpNRJ/fWAr2srAAEjZfwLNhvg6moMlJHh40mnnjiQYmzeLRzrd47NY8jLm8VlR+Ujw2G7tefaFbK55YCCpbigrdh04Vqun6QUuOq4H8R2OvJu2yQRnnOHg1FMdzJkTwLRpIUwvKyUPSNq3kG6h9evDk9piy1/GNWGwZm9nTh/hD9Sf6fF/8/eHqVPzOe+8Zvj7G3zxRQ6JifU/YQfo08fJ+d/nsT4NxhXN5YU+NVu/yVVMT3/Id4eTEmkDms4sEREREZH6rF4l7VUVEBDAc889h8PhYMOGDXz88ce0aNGCHj16HFJ2zJgxjBo16sD3Jl+f5VTDnBlbANiU3ourfXxG9v8ym8FpdCLHCyuKfJsQRBSt4v3mMDO5O6cdH0aHDg4uPnUokz+aR/dOvWGCE1fxDrJbPMGVV9jY6Uo7Zp0WC5x9toMhQ8p45v2+DO+0gpZlTWRtsGHQuWgV77WAcz9/ieNurH8J+9969XKxeHEmwcFeIiPr95T4fwsMhKDiCwjlexzOtmAY1ORZdI6iEgAK7WGEhiphFxEREakv6tX0+LCwMMxmM/n5+Qddz8/PP2T0/d/MZjOxsbG0a9eO0aNHc9xxxzF79uzDlrXZbAQFBR34CgwMrLkXUA98t+8vvi+GfaZ2WCy+juZQbf2uh7dW0mP72oo9YHgJSv8Ek7vkX9eOkGh5nfjteBzbhusIyPwOk6f0iNWG567g6nCIWX43w4aVJ5jH9TdD8smsWxGJYUBK1lJuyoLxmR4SEir4AoGoKINO+8ZyXTiE5x872W8M/PN+J8Czl0J7KGXhx1Hf/7OKj/c0qIT9b0Pbn03RJCeeP5bWaMIOkJKTwnsF8FOJuV4c1SciIiIi5epV0m61WklISGDjxo0Hrnm9XjZu3EjnzhXfVM3r9eJy+fZ88jrjdROc9i6B+6eD18XjOfs4Zx9kN6ufMwiO6xUG+/qzcV3IMcta7CmkLjmLmxbdT8DOSaxaaWb3ytdZPP9E8B56fnZS7hYG/vkerf76kX1rbqTFn72I3HQjAVk/g8dxoJzJVUCQfS0A6d4TiYgoT94SE134+xvk5VnYs7MET1Eao4Mh0dmShITKbapWYu0GQICjaWyIuGHrC+x2wYeLr2Lo8Hr4aVEjMfx4Dxhmlizxw1vDg+Frc/ZxbSZMKW3CRxaKiIiI1EP1KmkHGDVqFL/99hsLFy4kLS2Nd999l7KyMkaMGAHAa6+9xueff36g/KxZs1i/fj0ZGRmkpaXx/fff8/vvv3PCCSf46BXUrdzd73Lmgkd59I978Rgm3OkXYc7sTte44b4O7bB69Sr/MGXXdg+ekqzDFzIMgtI/J2z5yYzZtoEviuHRXdlcetM+Tlv7FFcl7yJrxwuHPBYd2g7D1gIX0CmsNWavncCs71jw17X89nN3jO2TAMje9wPfFHtZmtaJLn1jDjzv5wcjRv7Fg89EM3NtTxJK7XwXB+P2jTuQ2FeUOboLqS5Y4dwJnsa12eH/MgyDiclpdNgNz2zrxogR9XdqfEPXu7eToCAvztIS9q77s0brtnsHw/azyN1xSY3WKyIiIiLVU+/WtA8dOpTCwkK+/vpr8vPzadeuHQ8++OCB6fHZ2dkHrUMvKyvj3XffJScnBz8/P1q1asWtt97K0KFDffQK6tb0HdNZ6gAPLiwWK8z6GO9+CwNmZwOHjkb7Wrt2Hsae9wTdRzzGNwt7cNHZcw66by7LIGLbPQTk/gbAS+068bEzjoKVb1C0tx1xOd1oE7cZS+r7WONG47dvBl68ODo/zvrlMeS8/Ac4nPSOaccNF/3JBf2/5onUd1nnKOO1iJ2M6Qw/pyzkvv3Q2W7nqeEHJ5jNOwbzlCMXqwPujZwDVig0OlT6dTZLCKTNbgCDzbkrCY9pvB8i5Zfl4y7qjtddQrT7Qjp0qH8/d42Fnx8cd8oqSoYMYuxaWN59HYZ/sxqpu6VnEHz+I+17O4EmsheDiIiISANQ75J2gDPOOIMzzjjjsPcee+yxg74fN24c48aNq4Oo6iHD4Ba/bFrGwMdL7uH++8PZv798anK3bvVzeYDJBLkx+Tye7+V4xzYu+tc9W94SvvnjCvrZ7AwM9KOw/X2c0Hoix7nNDLw/FjCR/s5iZtx3JonWFXy+4HQezIE7I81YV93Fbfd3xe0uT2C25sDtj4/kdtNwml0QRcvEz+kc8SAAGc5TMWUlk7HzfPrfcHCCObx3KzJSmzMyJhN/U/mJBa7Ayiftvbv5E7MwEqvJRKbLSXiVeqsGGAbW0u0EZP+KuXAdhYnv/LMWuoY2MguxROJ451cozOO6Zyw1vdRa/sfAnm2ZWmaiDIPUvbOJT7i2RuotKiqfeBUa2vDW+ouIiIg0ZvUyaZeKsZbuIsaUzVVB/tz+9SSWugIAaNfOTUhI/f3Du03QOVwe+iInBDoxO3Px+kUBMC15CZP22+nk78ev587GL7w3AL//7k9WloWoKA9DhgRx28ev8dfjx1HgNdjjhuf2J5D2RFfAxJgxpTzySCELF/rz/feBLF7sT/aMyTBjMqdR/mFGTMwNGItvp99Jpfj55R8U24ABLrLnXc39XabQLhlCzHBms8qv0e7Y0U3uWVl43BaCzt1PnR6f5XXhV/AXAdlzCciZy1eZKUzJg4tC4br2W1iespivNjxH+9AW3HDWkmo1FZgxi92rsykrupXmIZGMHp1RQy9CjuTEYSbyvh3Ng0O+I8q5i6Iaqrev+QnSXv2Kn5NvBa6ooVpFREREpLqUtDdgfvnla1qX7BjKxeM9mM0lrFtn47LLSo7xpG8N7tqTk0o60Tl8BwWZsyiJnwDAhd0n8NG2rxjf/WqsYT0PlJ8+PQiAMWPs3HBDMcOHD2DYY3/Sb1Ae1qQ9pK28CjBx992F3H57MSYTXHSRnYsuspOfb+KXXwL44YfyBH7LFhtbttgAOHH4obMRmjf3sjN/EAUe2PP/J+ZdH1/5kXZ/f+jYwcu2bRa2bLHRqlXdrPO2ZnzHH6vu5LQAOyH/v2NFiWFlg9ONu7AtG+7tyg73YtYMdDDQkcEN1WnM8LJp09P089/LJUOjCex9BX5+NfEq5GgSE1188vSVtB3+HaUZCzB1yMWwRVW73neL5/KHO50Tgn/nLCXtIiIiIvWGkvYG7MftM8kvgK07+3DTncW0bl25Hc59pXdvF6s+60+L5jv4ec0jnOYfhyPmTCIDIllw0Z/4Wf7J/AoKypNugLFj7cTFebnttmKeeWYIf+38uz4nd9+dw0knHZoYR0QYXHyxnYsvtpOXV17X998HUlhoZsyYw28Q54nsh9UEn7SAG16eTY9pQUDlz7zv1s3FvpRSivesA7pW+vmqeHzNW0zba+eblnBcs3HM3TKad+b0h8IVbNlxFlvsUXToOoynz4B4U/V2eXdl/cqo5L2YDDC2DWXRc0c+Yk9qjsUCjvCh5BRFYQtKZeviU+gx+HPcIdX7Gcvy5LPTBQPM9W5/UhEREZEmTUl7A/b63jSW2KG32csDDSRhB2jTxsMP+wawPu9LnsmD71J/oX/MmQAHJewAP/wQSFmZiS5dXPTsWT4yft11xaxfb6OszMTEicUcf7yzQuuoIyMNxo2zM27c0Xdzb58YRMwuE3YMhkfk07Zt5RN2gI7ddmMM68FbLjivbB3U0IZhR5NGdzDWMWHOXeTPeu5fd7rQq5eTU04pIjK4C/dHAZSQ7nGAJaBKbWXsepMoMxSXRjDiuM7ExOiosLrSd1AgIyb/RssbhzLfmcEbjjGcfeZaMFf9gPVbA+K5v3kaq/cNq7lARURERKTalLQ3NP/aPMyU9R/I+I0462U+DqpyTCb4xRzLqjyIIZa8oCvJzDz86N7XX5dPjR871n4gMff3h3feyau1+AYPMNFicTTFfnkExqUTGFi1erp3ieaV3SacGKRnLiau9fk1G+hh2H5+B2a/R77XQkCAwfHHl3HqqQ5OPtlBy5bl6+rXrQ2iLMsPf5sTiysbjyW+0u1YSpMYVLaCbW2g4+OzuHaaRtnr0vHHl/HIpO7sWH0OlsSvadf5zmol7AAJNhedAiHVv20NRSkiIiIiNUFJewMSkPktxWlfYu3zCZitlP52F6x5gNOn5gEN6yzw42JHs+qtVWRld+VKV9BRy5rNBuefX3dJYbduLozrFnN9v+msKppAVabGAyR2N7hpSx9Gd1xDrCuzZoM8DJMrlx07moHbj/vvL+Taa0sIDDx0Q8LmLbxs2R6DJXwvIcW78Q+ofNIevPdDAH5ZfzZxzYaQmJhT3fClErp0cXPdBCdvv/0FLHyEZ/9I4JVX8ggPr/oGlAHm8pkSloDQmgpTRERERGqAFi82EI7SNO5Y8B+GrF2MY/skSvNyWb++fEO1oUMb3rnYo0eVEenojdkTiNlsHPHLYjG47LJSYmPrbvd1qxXiWiUwefYjxLYOrnI9cXFeWqadxUlBELRvPmE7J5XPlKgNhkHU8lOYdXU3urTcyqmnOg6bsAM0a+bl3IIceqXAsj2rKt2UyV1E0u7PMQx45Zf/MGFC/d74sLF69NFCXnopH/+ibsybF8Cll0ZX68frV3cWM4rAUbXVEiIiIiJSSzTS3kD8+Fsss/c1pzg4nRU73qfr7k/Z/HwsEz/+hVatwnwdXqX17Oli48b6ezzY+PGlrFnjx1lnVX0Gg8kEBaZuJLvgucw/6Z71Jxe3GIMrtFcNRlrOXbiellsz6OtnJrk4nLZtj7zHgc0GAWXRhPtlklFU+c/t9uRvZehuO51NARQXHsfppxdXJ3SphosustO1q5uPvriDsFar2bn9djp1GVWluu7OKyEfeLVt3Zx0ICIiIiIVo5H2BmL7xkgKP/qFWzPO47xgGJLmpG9hCtE9Dz22TKrvggvs7Nq1j2HDqjeLIS+wH2sKQvnTbuGF/HAenxLPp58G1fiI+9qkz8jzwrpSG5HBMUccZf+befYuCiY5ieHuSre1rmA/uAPZvuNErrnCwFK9Teilmnr1crG+3Y+87b+FtWlLqlyPI/ksSBlGRGSXGoxORERERKpLI+0NxD33FLFmTSdef/srRrTpT7B5I263HyMHtAIcvg5PjqBVQjQXLBqNCTDWXs3MIn8ujjiXb+ckc+4ZS6nQtvcVcBq72dQGHphzNVntj32SQGwLg61bOOIGgEcTue8CjBcuJCAyh/HztAFdfdDd3YbEqHzCnVU/RcI2cyaOIjNtr8oAGs5pFCIiIiKNnUbaGwirFV5/PY/oGAs3TfmFh3feRKvpsxvkevamZGA/YOZnGDM/Iyp/JJ62W3gpfDEPpadiFG2okTZMnlL8C1fQ3R+2/n477dode+O8mJjyPQIqm7QHpX3A3BkpYI/i4lPjqrXxmdScc8pO46uW0Nd09E0dj8TrheLi8g+QwsL0byoiIiJSnyhpb0CaNfPy5pu5ZBW35LaPplFqP/XAMV5SPyUmunn//Vw++CCHVasyuPKU4+ngDueKMPDu/7ZG2vDLX4LJcJJR0pbt+zrTrt2xR0nbdH6XC58OJDeya4XbMZfsoGTrQ7x5zkBiI/ZxzTVay15fOM0xAFicWVV63p21hZRXWjP/vyMJDdXvFBEREZH6REl7AzNwoItJkwowmQxGj25Yx7w1Vaef7uC008rw84NzR7sYNP91XoqB5tlzj7q23VKaRHDaewTt++Ko9c/b/jF3ZcH7Sb0BE+3bH3uk3RPsYHqZg8XO7H8uGl4CM2Zhse8+7DMrNj5N22Q4fUs83fpG0rGjplDXF16/8qTdz6ha0r5m71pGFu7liYAVBGj3eBEREZF6RWvaG6Crripl1CgHUVEaEWto2rXzkOw8HYfTnwB2YS3ZgjukOwAmjx2//CX45y4gIGcBVsduns+DpXaYemZ7LJHHHbbOL/JL+TYfuqaF/n8bx07a24X34xkvtLYYYHjBZKZ037dMXHALp4cEMP6Ej3FFDTtQ3uQu5K+0+XiAbXv68oyOeatXtgXsJT4ZWplWMasKz2cW7WenC0C/U0RERETqG420N1DNmnkx61+vQTrlDCs/rzuTrU5I3/3Rget/rX2Ax+dfQcmeD7A6dmOYbOx0+zOzBJbuOvJo+3ndr2NMu4vZOv92gApNj28b2537ouCScA9mVz4ABbkrsBtwW6aD6UsmgOefmRxB+79mcrSLmf4JWDY8z4kn6liw+iQgqDl73bDPW7U9Lrqam7M4Hu6gRw1HJiIiIiLVpbRPpI6NHu3gkWwLPfbAS9vmHbj+WPIaXsmHD0uO55V1X9DzsQzmrD6ZtkUdcJhHHrG+09qexnUtXoX0ATRv7iE4+NgbicW0sJBdFA2A2ZkBQHxhNucEQ0+rH0O6vgWWQAACM2YTnPIGAHPm3sv145rpA6N6JiHmVGzv/0HA7B1Vej7Q5eWEQOjoaVPDkYmIiIhIdWl6vEgdi4/3YORcRnSnWZxm6s2OHVbmz/eneOd1mEu3cf+KibB3cHnh7T8CsL95PvQ79Hi1wP0z8AS0Zk/yUKBiU+MBmjf3siUvhnC/HGw5ewgP6YalaBu3R8J3b85mzDun8sknOXiitnDhT3fyVkwZI00RfL/hEha/pQ3o6pv4FsG4UoaRb/cAGZV+3igrgkAo84bVfHAiIiIiUi1K2kV84PxBw5m84UKuW3IfpenN///qPQC0auVm5GUlnHRSGcuX+/HmmyEsXerHZZf9T9LudfL7mnsZYCujLGMh0KJCU+MBgoMNJtr3si0F3gxYxuj4E7Gbd2EYsCWtD/vzrZx7bjPOu62EbMq4cm8gsSuu49wLTAQF6Uiw+ubvI/xycsx4PGCxVO75baW7SHPDXlz0r4X4RERERKTqlLSL+MC5o51MefoLSp0m/PwMjjuujJEjy786dnRjKj8ym7AwL7O/LCLa8y2F6WbC4kYdqKM0exFj0srwAufsaAVUfKQdwM/ZnAh/B9mlAeQWJdFzt0EYZu69H6Z/4WTdOj8+ePIkiNpAcWAOu7J78Nl8bUBXH0VHexl/5SWEtlrDru130LnbqGM/9C+zi3czywlnmtIYW0sxioiIiEjVKGkX8YGWLb3Mnp1Nbq6ZwYOdRxy97tvXSYeLr+SDlvMIWd6ee8/7JxnL2/8r/f2h2BLGvh2tgcol7YELNpK/LIDoN3JJsi8Bj5XCwtaMuNDMxRfk8NprIezYYSUvrxN5eV0YdaOd+HhNja+PrFZY1/ZnNpvz6b53aaWT9uzc8yDXQlGIUnYRERGR+kZJu4iP9O7tOmaZgAAwOwfhZR4ppalgeMBUPve5t3M9y9vA/k6P0uu58v+U27ev+NnpLZqXf1CQmWmhR+lAeKoE/+h02tzlwWKBe+8tqsKrEl/p7m5Dz6h8wl2VP7YtIesWfv/oPgbcXgTo311ERESkPtEe0CL1XJ/I69jSMpiZcW6sxVsAMDuz8CveCEBJ0ClkZZUn8m3bVnykvXnz8gQ/Owu2bzOBx4+uLeMqvR5a6ofznKfxZUvoQ0Clny0qKl+PERqqc9pFRERE6hsl7SL13PAhwSTvOgEAv/y/ADCy5uE1wBmSyK69sQBERXkID6/4JnGxracz9plASlq3Z4xlIFuf68LI/ptq/gVInSgzxwBgcWZX+tkHjxvNxik96BS9uqbDEhEREZFqUtIuUs/16+fkzx3DAXCnLwPgvdTVtEiGZwpD2b278lPjAUyhXmY4HPzmyuPWvBQ+9d9OaGtbzQYvdcbrV56027yZlX72HvefXOXdTKataue8i4iIiEjtUdIuUs8FBMAOvzZMzID/JM0Dw0tI1ECyPUBYzwNJe2U2oQNoF9OHZ6Lhlgj4vAiezoUOHaNq/gVIndgWsJdWSXBlwapKP7vZ7WRlGVgDg2ohMhERERGpDm1EJ9IAhLXuxLuFYMXNI3npZM69muYZG/lu/kPs3R4MQPv2lUva28bGM94diN1qxwYs3ZdAYjczoHXNDVFgcAzpBWDl2BscHsTwMqOlQYEBrojOtROciIiIiFSZknaRBuC0Qe3ZtfB4+gX4cfJZiaSnBgHv8u+J0L16VS5ZaxFrsH9TLAnNk7ktEsyrxhAbq4S9oUpodhq2F//E1iwOrqn4cyZPMcf//wD771GxtROciIiIiFSZknaRBqB/fxdLLl/MgrLyXb5btvRwyy1FREeXJ9nR0V6GDHFWqs7ISC/rC5pRFJ5MayvkebtiMtV46FJH4mODcaUOJb/UA2RU+DmTq/yItzKXHyFhfmimhYiIiEj9oqRdpAEICICLLirlhx8CmDChhBtuKCEwsOI7xR+O2Qz3e5LZkAIvx4AntGsNRSu+EBNTnmzn5ppxu8Fawd/u2Xn7WVIMZkcAPcOq9zMlIiIiIjVPSbtIA/HMMwU880xBjdbp72oOlmxuy4K7W2qEtSGLivIy/qpxhMatZef2O+nafVSFntuWlc7YfRCNnXUBStpFRERE6hvtHi/ShAWu+wlyE8AVwKAu2oSsIbNYYG2bObztt40N6X9V+LkCTxdIGUph0ulaHiEiIiJSD2mkXaQJaxPVDF7ZBVYH3dfm+TocqaZET1t6h60n3FnxWRPNvD3g/T+Ja+sGKn/Gu4iIiIjULo20izRhzZuXJ3exzWxERmpqdEN3nvNUvmgJvYyACj9TVFT+NhAaquURIiIiIvWRknaRJiwuzgNAt26VPNtb6iWnOQYAizu7ws/EOz5iwzOJ3DLi6doKS0RERESqQdPjRZqwUaPs7NplZcyYUl+HIjXA61+etPt5Kj7N/aeCn5lvbCIxMpzRTKyt0ERERESkipS0izRhYWEGDz9c6OswpIZs8U+jZRK0Ma9kVgWf2evKZGUZtDBptoWIiIhIfaTp8SIijURQcHP2eyDDU/EE/Dy/eL5vCSPMg2sxMhERERGpKo20i4g0Eh1iTsP2whIsUXEwoYLPmKFvCBRbdeSfiIiISH2kpF1EpJGIjw3BlZpAbpEX2F+hZ6L8dgPgtUXXXmAiIiIiUmWaHi8i0kj8fYRffr6Z5B3vYc2Zf9TyJkcGM72bWWmHbPPAughRRERERCpJSbuISCMREeElNNRLv1H3cOqiR3hu4QQspUlHLL89ezV3ZsPxaWb8IkLqMFIRERERqSgl7SIijYTZDA8+WMi6lN7YDdjkcBK24RpM7uLDlt9vbwvbRlO2aQzDBpvqOFoRERERqQgl7SIijcgVV5QyvtcYImd9xttBsQTadxCx9TYwvIeUzdrQH774jsQtnxMdfeh9EREREfE9Je0iIo3ME08U0Mn/AsZOnUWZ24/A7DkEJL90UBmTu5BVS0sAGD68zBdhioiIiEgFKGkXEWlk/Pzg7bfzSC0dwA3vv87kXDjvzxcxZf50oIwj5RNeOb010666iRNOUNIuIiIiUl8paRcRaYRiYry8/34eX2w+lSez/PjdAT/vnHHg/ivrvyQ6Gb7238GgQU4fRioiIiIiR6OkXUSkkerZ08WLj4Xi/Go6iWtuwZH5RfkNr5udJSm4ANyDCAjwZZQiIiIicjRWXwcgIiK157zz7GzePJJp087hrl+8dOjgpn/rv/guzs3KgjCmu6/1dYgiIiIichQaaRcRaeTuu6+Ik05y4HCYueu2YibPvYAUN+zZfiqnnBDs6/BERERE5CiUtIuINHIWC7z2Wh4JCW5Kh01kaj7cmQVLk0fSvbvb1+GJiIiIyFEoaRcRaQLCww0++CCXzDnv0rwkDm9BPCkBIzHrXUBERESkXtOfayIiTUTHjm5efzqUzHcXM3vmSxw3KNbXIYmIiIjIMZgMwzB8HUR9kJWVhcvl8nUYIiK1bvr0QBYv9mfKlAKCgvQWICIiIlLXbDYbMTExFSqrpP3/KWkXERERERGRulCZpF3T40VERERERETqKSXtIiIiIiIiIvWUknYRERERERGRekpJu4iIiIiIiEg9paRdREREREREpJ5S0i4iIiIiIiJSTylpFxEREREREamnlLSLiIiIiIiI1FNK2kVERERERETqKSXtIiIiIiIiIvWUknYRERERERGRekpJu4iIiIiIiEg9paRdREREREREpJ5S0i4iIiIiIiJSTylpFxEREREREamnlLSLiIiIiIiI1FNK2kVERERERETqKSXtIiIiIiIiIvWU1dcB1BdWq7pCREREREREal9l8k+TYRhGLcYiIiIiIiIiIlWk6fF1yG63c99992G3230dSpOjvq9b6m/fUd/7jvq+bqm/fUd9X7fU376jvvcd9f3BlLTXIcMwSE5ORpMb6p76vm6pv31Hfe876vu6pf72HfV93VJ/+4763nfU9wdT0i4iIiIiIiJSTylpFxEREREREamnlLTXIZvNxtixY7HZbL4OpclR39ct9bfvqO99R31ft9TfvqO+r1vqb99R3/uO+v5g2j1eREREREREpJ7SSLuIiIiIiIhIPaWkXURERERERKSeUtIuIiIiIiIiUk8paRcRERERERGpp6y+DqA+mDVrFsuXL2fv3r34+fnRuXNnLrvsMuLi4g6UcTqdfPzxxyxZsgSXy0Xv3r259tpriYiIAGD37t3Mnj2bbdu2UVhYSPPmzTn11FM566yzDtSxadMmJk2adEj7b7/99oF6DscwDL7++mt+++03SkpK6Nq1K9deey0tW7Y8UObmm28mKyvroOcuueQSzjvvvKp1Sh1oDP0OsHr1ambMmMGePXvw8/OjW7du3HvvvdXrnFrQ0Pv7SPUCPPXUU3Ts2LEKvVI3GnrfA6Snp/Ppp5+ybds23G43bdq04eKLLyYxMbH6HVSLGkPfJyUl8dlnn7Fr1y7MZjODBw/myiuvJCAgoPodVMPqe38vW7aMuXPnkpSURHFxMc8++yzt2rU7qMyx4quvGkPfz5s3jz/++IPk5GTsdjsffPABwcHB1eqX2tLQ+7u4uJivv/6adevWkZ2dTVhYGAMHDmTcuHEEBQVVu39qU131PYDL5WLGjBn8/vvv5OfnExkZyQUXXMBJJ5101BjnzJnD999/T35+Pm3btuWaa6456O+UhvSz/m8Nve8b8s+9knZg8+bNnH766XTo0AGPx8MXX3zBk08+yYsvvnjgj6KPPvqI1atXc+eddxIUFMR7773HCy+8wBNPPAGU/1EVHh7OrbfeSnR0NNu2bePtt9/GbDZzxhlnHNTe1KlTD/rBCAsLO2p83377LT///DM333wzzZs356uvvmLy5Mm8+OKL+Pn5HSh30UUXccoppxz4vj7+QfdvjaHf//rrL9566y3Gjx9PYmIiXq+XlJSUmuymGtPQ+7tLly68/fbbBz3z5ZdfsnHjRjp06FATXVRrGnrfA0yZMoXY2FgeeeQR/Pz8+PHHH5kyZQqvvvpqvU5mGnrf5+bm8sQTTzB06FAmTJhAaWkpH330EdOmTeOuu+6q4d6qvvre32VlZXTt2pUhQ4bw1ltvHbbMseKrrxpD35eVldGnTx/69OnD559/Xp3uqHUNvb9zc3PJzc3l8ssvJz4+nuzsbN555x3y8vLq5e+Wf6vLvn/ppZcoKCjghhtuIDY2lvz8fLxe71HjW7JkCR9//DETJ06kU6dO/Pjjj0yePJmpU6cSHh4ONKyf9X9r6H3fkH/uMeQQBQUFxoUXXmhs2rTJMAzDKCkpMcaNG2csXbr0QJm0tDTjwgsvNLZt23bEet555x3jscceO/D9xo0bjQsvvNAoLi6ucCxer9eYOHGi8e233x64VlJSYlxyySXGH3/8ceDaTTfdZPzwww8Vrrc+amj97na7jeuvv9747bffKlxvfdLQ+vt/uVwuY8KECcb06dMr3E590dD6/u94N2/efKBMaWmpceGFFxrr1q2rcFv1QUPr+7lz5xrXXnut4fF4DpTZs2ePceGFFxr79u2rcFu+Up/6+98yMjKMCy+80EhOTj7oelXjq48aWt//W3Xb8IWG3N9/W7JkiTF+/HjD7XZXqS1fqa2+X7NmjXHllVcaRUVFlYrngQceMN59990D33s8HuO6664zZs2adUjZhviz/m8Nue//1lB+7jXSfhilpaUAhISEAOWfCHk8Hnr27HmgTKtWrWjWrBnbt2+nc+fOR6zn7zr+7d5778XlctG6dWsuvPBCunbtesRYMjMzyc/Pp1evXgeuBQUF0bFjR7Zv386wYcMOXJ89ezbffPMNzZo14/jjj+fss8/GYrFU7sX7UEPr9+TkZHJzczGZTNx7773k5+fTrl07LrvsMtq0aVOlPqhLDa2//9fKlSspKipi5MiRFXvB9UhD6/vQ0FDi4uJYtGgR7du3x2azMXfuXMLDw0lISKhSH/hKQ+t7l8uF1WrFbP5nC5q/Zz9s3bqV2NjYSrz6ulef+rsiqhpffdTQ+r6hawz9XVpaSmBgYIP62xFqr+9XrlxJhw4d+Pbbb1m8eDEBAQH079+fcePGHTTT9d/cbjdJSUkHLU81m8307NmT7du3V/el1juNoe8bys+9NqL7H16vlw8//JAuXbocSLzy8/OxWq2HrDUJDw8nPz//sPVs27aNpUuXHjRdPTIykokTJ3LXXXdx1113ER0dzaRJk0hKSjpiPH/X//d0miO1feaZZ3L77bfz6KOPcsoppzBr1iw+/fTTSrxy32qI/Z6RkQHA9OnTOf/887n//vsJDg5m0qRJFBcXV+bl17mG2N//a8GCBfTp04fo6OhjvNr6pSH2vclk4uGHH2b37t1ceeWVXHrppfz44488+OCDh/3jsr5qiH2fmJhIfn4+3333HW63m+LiYj777DMA8vLyKvPy61x96++KqEp89VFD7PuGrDH0d2FhId98881BbTcEtdn3GRkZbN26ldTUVO655x6uvPJKli1bxrvvvnvEeAoLC/F6vYcsG4uIiGhQv0MqojH0fUP6uddI+/947733SE1N5fHHH69yHSkpKTz77LOMHTuW3r17H7geFxd30EYNXbp0ISMjgx9//JFbb72V33///aA1u//X3v3HVFn+fxx/HYQjhyCOCEdKkEJgOl2EiWURQeCahW4pUblauVw1K7e2Rqu1GpVktuXckH8aNJwDI/APJv1YZERhs5YlHkosygrE+KFAdhAEzvcPv94fzgc+ikzwvvH52M7Gue/73Oe6Xtw7536f6/7x8ssv+4yuXEhW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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "## Plots\n", + "import matplotlib.pyplot as plt\n", + "\n", + "## Delta Comparison\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(old_data.index, old_data[\"Delta\"], label=\"Old Delta\", color=\"blue\", )\n", + "plt.plot(new_data.index, new_data[\"Delta\"], label=\"New Delta\", color=\"orange\", linestyle=\"--\")\n", + "plt.plot(binom_data.index, binom_data[\"Delta\"], label=\"Binomial Delta\", color=\"green\", linestyle=\":\")\n", + "plt.title(\"Delta Comparison\")\n", + "plt.xlabel(\"Date\")\n", + "plt.ylabel(\"Delta\")\n", + "plt.legend()\n", + "plt.grid()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "a198d448", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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r981rztq1KirHmvXrp1t9kJ5UjTOdu97o5o3b07btm2ZOHEijz76aIljeXl5PPnkkwwePLjUecXJmBYtWvDzzz+zY8cOateujZubG82aNWPr1q1s3779momY8PBwsrOzSU5Ots0ecHFxITw8vNQSkalTpzJ9+nTeffddYmJicHZ25p133sFoNJbo98/zVCpVibbiZMQ/p///8zydTldqnCvPGTRoEFWqVOE///kPgYGBWCwWOnToUCoeIYQQQggh7nWRej3pbn6EmxOZN8/6cK5Ll0KmTs3g669deP99Dw4dckCtUvPiuhfJN+XTI7wH0V63tgRa3LgKt6xA3NveeOMNVq9ezV9//VWivU6dOpw4cYLw8PBSP8UzOIrrDixZsoSWLVsC1oTBpk2b2LVrl62tLD169ECn0/Hll19eN8Zdu3bRpUsX+vTpQ+3atQkNDSUuLu427vrWpaenc/r0aUaNGkWbNm2Ijo4uVTxTCCGEEEKIf4tPw2uxNzCF+1UJ/P67NTnwyCPWIuV161ofjh06pEOtUtMjogePRvf51+76VtFIckDcUTVr1uThhx8uUXAPYPjw4ezevZs333yTQ4cOERcXx8qVK20FCcE6Hd/Dw4OFCxfaZoK0aNGClStXYjAYrlpvAKyzD95++22mT5/OqFGj2LJlCxcuXODgwYNMnz4dsC4NAOssg40bN7Jr1y5OnjzJ2LFjSU1NvdMvxQ3x9PTEy8uLn376iTNnzrB582beffddu8QihBBCCCHE3aY2pgMQlxBAeroGPz8z991XBEDt2tbkwMWLWixxC5itXceMADVRnlF2i7cykeSAuONee+21UlPta9Wqxfz584mLi6N379506dKFTz75hICAAFsflUpFs2bNUKlUNG3a1Haem5sbdevWxdn52kshBg8ezOzZs0lPT+fZZ5+ldevWDBgwgAsXLvDzzz/b6gWMGjWKOnXq0L9/f/r27Yufnx9dunS5w6/CjVGr1Xz11VccPHiQjh07MmHCBMaPH2+XWIQQQgghhLjbNAZrva7te4MAePjhAopX5Xp4KFSrZgLg7EVPNMYUdLmHyhxH3HkqRVEUewdR2aSkpJRaT56dnY27u7udIhLixsjvqRBCCCGEuFWJeYk8uaA1waoCQpbO4seNT7JqVTK1a5tsfYYO9WLZMic+ffcQr0TVQVFpOdJ0J56O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IiIiIiIiUsdZq7oDdZHVqmEXERERERGRC+9M7z9NhmEYF7gvIiIiIiIiIlKNaVpBLVRcXMwjjzxCcXFxVXelztHYVy6Nd9XR2FcdjX3l0nhXHY195dJ4Vx2NfdXR2Jen4EAtZBgGu3btQkkhlU9jX7k03lVHY191NPaVS+NddTT2lUvjXXU09lVHY1+eggMiIiIiIiIidZyCAyIiIiIiIiJ1nIIDtZDNZmP48OHYbLaq7kqdo7GvXBrvqqOxrzoa+8ql8a46GvvKpfGuOhr7qqOxL0+rFYiIiIiIiIjUccocEBEREREREanjFBwQERERERERqeMUHBARERERERGp4xQcEBEREREREanjrFXdgbpkxowZrFq1igMHDmC322nevDm33noriYmJ/jZOp5MPP/yQ5cuX43K56NChA6NGjSIyMhKA3bt388UXX5Cenk5eXh7x8fEMGjSIIUOG+I+xadMmnnrqqRPO/9Zbb/mPUxHDMJgyZQoLFiygsLCQli1bMmrUKBISEvxt7r//fjIzM8vtN3LkSK655pqzG5RKUhvGHmDt2rVMnTqVPXv2YLfbadWqFePGjTu3wbkAavp4n+y4AM8//zypqalnMSqVo6aPPcDBgwf56KOPSE9Px+1207BhQ2666Sbatm177gN0AdWGsd+5cycff/wxO3bswGw2c9FFF3HHHXcQGBh47gN0HlX3sV65ciXffPMNO3fupKCggBdffJHGjRuXa3O6/lVXtWHsv/32W5YuXcquXbsoLi7mvffeIyQk5JzG5UKp6eNdUFDAlClTWL9+PVlZWYSHh9OtWzdGjBhBcHDwOY/PhVRZYw/gcrmYOnUqS5YsweFwEBUVxfXXX8+AAQNO2cf58+cza9YsHA4HjRo14u677y73HaUmvdePV9PHvia/7xUcqESbN2/m8ssvp2nTpng8HiZPnsyzzz7Lv/71L/8Xrw8++IC1a9fy5z//meDgYCZNmsQ///lPnnnmGcD3xS0iIoI//OEPxMTEkJ6ezltvvYXZbOaKK64od75XXnml3BswPDz8lP378ssvmTdvHvfffz/x8fF89tlnPPfcc/zrX//Cbrf72914440MHDjQ/3N1+9JYkdow9j/88ANvvvkmN998M23btsXr9bJ3797zOUznTU0f7xYtWvDWW2+V2+fTTz8lLS2Npk2bno8humBq+tgDTJgwgfr16/P4449jt9uZM2cOEyZM4LXXXqvWN041feyzs7N55pln6NGjB/fccw9FRUV88MEHvP766zz00EPnebTOTXUf69LSUlq2bMkll1zCm2++WWGb0/WvuqoNY19aWkrHjh3p2LEjn3zyybkMxwVX08c7Ozub7OxsbrvtNpKTk8nKyuLtt98mJyen2v1e+bXKHPuXX36Z3Nxcfve731G/fn0cDgder/eU/Vu+fDkffvgho0ePplmzZsyZM4fnnnuOV155hYiICKBmvdePV9PHvia/7zGkyuTm5ho33HCDsWnTJsMwDKOwsNAYMWKEsWLFCn+b/fv3GzfccIORnp5+0uO8/fbbxpNPPun/OS0tzbjhhhuMgoKCM+6L1+s1Ro8ebXz55Zf+1woLC42RI0caS5cu9b923333GbNnzz7j41ZXNW3s3W63MWbMGGPBggVnfNzqpKaN96+5XC7jnnvuMT7//PMzPk91UdPGvqy/mzdv9rcpKioybrjhBmP9+vVnfK7qoKaN/TfffGOMGjXK8Hg8/jZ79uwxbrjhBuPQoUNnfK6qUJ3G+nhHjhwxbrjhBmPXrl3lXj/b/lVHNW3sj3eu56gKNXm8yyxfvty4+eabDbfbfVbnqioXauzXrVtn3HHHHUZ+fv5v6s+jjz5qvPPOO/6fPR6Pce+99xozZsw4oW1NfK8fryaPfZma8r5X5kAVKioqAiA0NBTwRbg8Hg/t2rXzt0lKSiI2NpatW7fSvHnzkx6n7BjHGzduHC6XiwYNGnDDDTfQsmXLk/YlIyMDh8NB+/bt/a8FBweTmprK1q1b6dmzp//1L774gmnTphEbG0uvXr0YOnQoFovlt118FatpY79r1y6ys7MxmUyMGzcOh8NB48aNufXWW2nYsOFZjUFlqmnj/Wtr1qwhPz+f/v37n9kFVyM1bezDwsJITEzk+++/p0mTJthsNr755hsiIiJISUk5qzGoKjVt7F0uF1arFbP5WDmismyOLVu2UL9+/d9w9ZWrOo31mTjb/lVHNW3sa7raMN5FRUUEBQXpu+Mv1qxZQ9OmTfnyyy9ZvHgxgYGBdOnShREjRpTL3D2e2+1m586d5ab1ms1m2rVrx9atW8/1Uqud2jD2NeV9r4KEVcTr9fL+++/TokUL/82dw+HAarWeMBcoIiICh8NR4XHS09NZsWJFuTT/qKgoRo8ezUMPPcRDDz1ETEwMTz31FDt37jxpf8qOX5aGdLJzDx48mD/+8Y888cQTDBw4kBkzZvDRRx/9hiuvejVx7I8cOQLA559/znXXXcdf//pXQkJCeOqppygoKPgtl1/pauJ4/9rChQvp2LEjMTExp7na6qUmjr3JZOKxxx5j9+7d3HHHHdxyyy3MmTOH8ePHV/hFtrqqiWPftm1bHA4HM2fOxO12U1BQwMcffwxATk7Ob7n8SlXdxvpMnE3/qqOaOPY1WW0Y77y8PKZNm1bu3DXBhRz7I0eOsGXLFvbt28fDDz/MHXfcwcqVK3nnnXdO2p+8vDy8Xu8JU+0iIyNr1O+QM1Ebxr4mve+VOVBFJk2axL59+3j66afP+hh79+7lxRdfZPjw4XTo0MH/emJiYrmCHS1atODIkSPMmTOHP/zhDyxZsqTcfOrx48eXe1J0KsOGDfP/vVGjRlitVt5++21GjhyJzWY762upTDVx7A3DAOC6667j4osvBuC+++7jd7/7HStWrGDQoEFnfS0XWk0c7+MdPXqUn376iT/96U9n3f+qUhPH3jAMJk2aREREBE899RR2u53vvvuOCRMm8I9//IOoqKizvpbKVBPHvkGDBtx///188MEHfPLJJ5jNZgYPHkxERAQmk+msr+NCq25j3apVq7PuR02jsa9cNX28i4qKeOGFF0hOTuaGG24462uoChdy7Mu+4z3wwAP+eg8ul4t//etfjBo1ih07dvD888/729977720adPmrPtR09T0sa9p73sFB6rApEmTWLt2LU899VS5J5GRkZG43W4KCwvLRcJyc3NPiE7t37+fZ555hoEDB3L99def9pypqals2bIFgK5du9KsWTP/tujoaP9Todzc3HJfvnNzc0+o8nu8Zs2a4fF4yMzMLPehUl3V1LEv60NycrJ/u81mo169emRlZZ3ZxVeBmjrex1u4cCFhYWF07dr1jK65uqipY5+WlsaPP/7Ie++95/+gTklJYcOGDXz//ffVfmUUqLljD9CrVy969eqFw+HwF32aPXs29erVO/MBqETVcazPxG/pX3VVU8e+pqrp411cXMzzzz9PUFAQf/nLX7Baa84tyIUe+8jISKKjo8sVgkxKSsIwDI4ePUrTpk156aWX/NsiIiKw2WyYzeYTnlQ7HI4a8zvkTNT0sa+J73tNK6hEZU/EVq1axeOPP058fHy57SkpKVgsFjZu3Oh/7eDBg2RlZZWbO7Nv3z6eeuop+vbty80333xG5969e7f/C2FQUBD169f3/7Hb7cTHxxMZGVnu3EVFRWzfvv2Ucx93796NyWQ6bTXbqlbTxz4lJQWbzcbBgwf9bdxuN5mZmcTFxf32AbnAavp4H38dixYtok+fPjXiFzrU/LEvLS0FOOFJt8lkOm314KpW08f+eJGRkQQGBrJ8+XLsdnu5WgXVQXUe6zNxpv2rjmr62Nc0tWG8i4qKePbZZ7FarYwbN67G/FtV1ti3bNmSnJwcSkpK/K8dOnQIk8lETEwMdru93NgHBQVhtVpJSUkhLS3Nv4/X6yUtLa3a/w45E7Vh7Gvq+75mfNutJSZNmsTSpUsZN24cQUFB/ohTcHAwdrud4OBgBgwYwIcffkhoaCjBwcG8++67NG/e3P9m27t3L08//TQdOnRg2LBh/mOYzWb/DfqcOXOIj4+nQYMGOJ1OvvvuO9LS0vj73/9+0r6ZTCaGDBnC9OnTSUhIID4+nk8//ZSoqCi6desGwNatW9m2bRtt2rQhKCiIrVu38sEHH9C7d+9qPxe4po99cHAwgwYNYsqUKcTExBAXF8fMmTMB/NMMqpOaPt5l0tLSyMjI4NJLLz3/g3SB1PSxb968OaGhoUycOJHhw4djt9tZsGABGRkZdO7c+cIN3HlQ08cefOs2N2/enMDAQDZs2MBHH33EyJEjq9262NV5rMG3xnVWVhbZ2dkA/sBuZGQkkZGRZ9S/6qqmjz34nvA5HA4OHz7s709QUBCxsbHV7vtMTR/voqIinnvuOUpLS/nDH/5AcXExxcXFgG+ZxN861a8yVdbY9+rVi2nTpvHGG29w4403kpeXx0cffUT//v1PeUM5bNgwXn/9dVJSUkhNTWXu3LmUlpbSr18/f5ua9F4/Xk0f+5r8vjcZZZMt5IK78cYbK3z9vvvu87+ZnE4nH374IcuWLcPtdtOhQwdGjRrl/0CbMmUKU6dOPeEYcXFxvP7664BvLetvv/2W7OxsAgICaNSoEddffz1t27Y9Zf8Mw2DKlCl8++23FBUV0bJlS+655x7/dIGdO3cyadIkDhw4gMvlIj4+nj59+jBs2LBqX2+gpo89+DIFPvnkE5YsWYLT6SQ1NZU777yTBg0anMWIXFi1YbwBXn31VbKysqr9uuPHqw1jv2PHDj799FN27NiBx+MhOTmZ4cOH06lTp7MYkcpTG8Z+4sSJrF27lpKSEpKSkrjyyivp06fPWYzGhVXdx3rRokW88cYbJ7w+fPhwf99P17/qqjaM/cnOf/w1VBc1fbw3bdrEU089VeG+EydOPOGJcHVSWWMPcODAAd59913S09MJCwvjkksuOWXF/DLz589n5syZOH5Zxequu+4qN/2jJr3Xj1fTx74mv+8VHBARERERERGp46pvToOIiIiIiIiIVAoFB0RERERERETqOAUHREREREREROo4BQdERERERERE6jgFB0RERERERETqOAUHREREREREROo4BQdERERERERE6jgFB0RERERERETqOAUHREREREREROo4a1V3QERERGqvRYsW8cYbb/h/ttlshIaG0rBhQzp16kT//v0JCgr6zcdNT09n/fr1DB06lJCQkPPZZRERkTpJwQERERG54G688Ubi4+PxeDw4HA42b97MBx98wJw5cxg3bhyNGjX6TcdLT09n6tSp9OvXT8EBERGR80DBAREREbngOnXqRNOmTf0/X3vttaSlpfHCCy/w4osv8vLLL2O326uwhyIiInWbggMiIiJSJdq2bcv111/P5MmTWbx4MQMHDmTPnj3Mnj2bn3/+mZycHIKDg+nUqRO33XYbYWFhAEyZMoWpU6cCMHbsWP/xJk6cSHx8PACLFy9mzpw57N+/H7vdTocOHbj11luJjY2t/AsVERGpARQcEBERkSrTp08fJk+ezIYNGxg4cCAbNmwgIyODfv36ERkZyf79+/n222/Zv38/zz33HCaTiYsuuohDhw6xbNky7rjjDn/QIDw8HIDp06fz2Wefcckll3DppZeSl5fHvHnzeOKJJ3jxxRc1DUFERKQCCg6IiIhIlYmJiSE4OJgjR44AcPnll3PllVeWa9OsWTNeffVVtmzZQqtWrWjUqBFNmjRh2bJldOvWzZ8tAJCZmcmUKVO46aabuO666/yvd+/enUceeYSvvvqq3OsiIiLio6UMRUREpEoFBgZSXFwMUK7ugNPpJC8vj2bNmgGwa9eu0x5r5cqVGIZBjx49yMvL8/+JjIykfv36bNq06cJchIiISA2nzAERERGpUiUlJURERABQUFDA559/zvLly8nNzS3Xrqio6LTHOnz4MIZh8MADD1S43WrVVx8REZGK6BNSREREqszRo0cpKiqiXr16ALz88sukp6dz1VVX0bhxYwIDA/F6vTz//PN4vd7THs/r9WIymXj00Ucxm09MkAwMDDzv1yAiIlIbKDggIiIiVWbx4sUAdOzYkYKCAjZu3MiNN97I8OHD/W0OHTp0wn4mk6nC49WvXx/DMIiPjycxMfHCdFpERKQWUs0BERERqRJpaWlMmzaN+Ph4evXq5X/SbxhGuXZz5sw5Yd+AgADgxKkG3bt3x2w2M3Xq1BOOYxgG+fn55/MSREREag1lDoiIiMgFt27dOg4cOIDX68XhcLBp0yY2bNhAbGws48aNw263Y7fbadWqFTNnzsTj8RAdHc369evJyMg44XgpKSkATJ48mZ49e2KxWOjSpQv169dnxIgRfPLJJ2RmZtKtWzcCAwPJyMhg9erVXHrppVx11VWVffkiIiLVnsn4dVhdRERE5DxZtGgRb7zxhv9nq9VKaGgoDRs2pHPnzvTv35+goCD/9uzsbN599102bdqEYRi0b9+eu+66izFjxjB8+HBuvPFGf9tp06bxzTffkJOTg2EYTJw40b+s4cqVK5kzZ45/hYPY2Fjatm3L4MGDNd1ARESkAgoOiIiIiIiIiNRxqjkgIiIiIiIiUscpOCAiIiIiIiJSxyk4ICIiIiIiIlLHKTggIiIiIiIiUscpOCAiIiIiIiJSxyk4ICIiIiIiIlLHKTggIiIiIiIiUscpOCAiIiIiIiJSxyk4ICIiIiIiIlLHKTggIiIiIiIiUscpOCAiIiIiIiJSxyk4ICIiIiIiIlLH/T9Zwj01IH4eSwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "## Plots\n", + "import matplotlib.pyplot as plt\n", + "\n", + "## Delta Comparison\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(old_data.index, old_data[\"Gamma\"], label=\"Old Gamma\", color=\"blue\", )\n", + "plt.plot(new_data.index, new_data[\"Gamma\"], label=\"New Gamma\", color=\"orange\", linestyle=\"--\")\n", + "plt.plot(binom_data.index, binom_data[\"Gamma\"], label=\"Binomial Gamma\", color=\"green\", linestyle=\":\")\n", + "plt.title(\"Gamma Comparison\")\n", + "plt.xlabel(\"Date\")\n", + "plt.ylabel(\"Gamma\")\n", + "plt.legend()\n", + "plt.grid()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "b1728243", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
DeltaVolgaRhoVegaThetaGammaMidpointCloseaskClosebid
datetime
2025-05-060.2853810.0149060.6473450.785890-0.0257840.0056619.20010.907.50
2025-05-070.2806180.0149320.6258520.769922-0.0258050.0055319.10010.257.95
2025-05-080.2950180.0133400.6557690.792500-0.0272900.00548310.00010.159.85
2025-05-090.2940870.0136650.6585220.794720-0.0269700.0055699.8259.959.70
2025-05-120.3536490.0089170.8331960.908825-0.0307420.00592912.82512.9512.70
..............................
2026-02-020.6501580.0019910.9043170.789516-0.0605870.00646430.77530.9030.65
2026-02-030.6454350.0016980.8896700.790056-0.0617880.00637430.87531.1530.60
2026-02-040.6878340.0037260.9566520.768999-0.0622510.00587135.67536.1535.20
2026-02-050.6803510.0030520.9342840.773474-0.0645000.00570236.15036.3535.95
2026-02-060.6970540.0042380.9641940.759992-0.0625200.00576536.75036.9536.55
\n", + "

191 rows × 9 columns

\n", + "
" + ], + "text/plain": [ + " Delta Volga Rho Vega Theta Gamma \\\n", + "datetime \n", + "2025-05-06 0.285381 0.014906 0.647345 0.785890 -0.025784 0.005661 \n", + "2025-05-07 0.280618 0.014932 0.625852 0.769922 -0.025805 0.005531 \n", + "2025-05-08 0.295018 0.013340 0.655769 0.792500 -0.027290 0.005483 \n", + "2025-05-09 0.294087 0.013665 0.658522 0.794720 -0.026970 0.005569 \n", + "2025-05-12 0.353649 0.008917 0.833196 0.908825 -0.030742 0.005929 \n", + "... ... ... ... ... ... ... \n", + "2026-02-02 0.650158 0.001991 0.904317 0.789516 -0.060587 0.006464 \n", + "2026-02-03 0.645435 0.001698 0.889670 0.790056 -0.061788 0.006374 \n", + "2026-02-04 0.687834 0.003726 0.956652 0.768999 -0.062251 0.005871 \n", + "2026-02-05 0.680351 0.003052 0.934284 0.773474 -0.064500 0.005702 \n", + "2026-02-06 0.697054 0.004238 0.964194 0.759992 -0.062520 0.005765 \n", + "\n", + " Midpoint Closeask Closebid \n", + "datetime \n", + "2025-05-06 9.200 10.90 7.50 \n", + "2025-05-07 9.100 10.25 7.95 \n", + "2025-05-08 10.000 10.15 9.85 \n", + "2025-05-09 9.825 9.95 9.70 \n", + "2025-05-12 12.825 12.95 12.70 \n", + "... ... ... ... \n", + "2026-02-02 30.775 30.90 30.65 \n", + "2026-02-03 30.875 31.15 30.60 \n", + "2026-02-04 35.675 36.15 35.20 \n", + "2026-02-05 36.150 36.35 35.95 \n", + "2026-02-06 36.750 36.95 36.55 \n", + "\n", + "[191 rows x 9 columns]" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "new_data\n", + "\n", + "## Convert to DataFrame for easier comparison\n", + "greeks = new_data.greek.timeseries\n", + "option_spot = new_data.option_spot.timeseries\n", + "data = greeks.join(option_spot[[\"midpoint\", \"closeask\", \"closebid\"]])\n", + "data.columns = data.columns.str.capitalize()\n", + "data" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/EventDriven/riskmanager/picker/__init__.py b/EventDriven/riskmanager/picker/__init__.py index 8230f88..2e66364 100644 --- a/EventDriven/riskmanager/picker/__init__.py +++ b/EventDriven/riskmanager/picker/__init__.py @@ -98,7 +98,7 @@ import pandas as pd import numpy as np from dataclasses import dataclass -from typing import Any, Dict +from typing import Any, Dict, List, Tuple from EventDriven.types import ResultsEnum from trade.helpers.Logging import setup_logger from EventDriven.configs.core import ChainConfig @@ -413,6 +413,15 @@ def build_strategy( def create_trade_id(legs: Dict[str, Any]) -> str: + """ + Creates a unique trade identifier based on the legs of the option structure. + + Expected input format for legs: + legs = { + "long": [ { "opttick": "AAPL230616C00150000", ... }, ... ], + "short": [ { "opttick": "AAPL230616P00150000", ... }, ... ] + } + """ def _iter_side(side): if side is None: return [] @@ -425,9 +434,13 @@ def _iter_side(side): raise TypeError(f"legs['long'/'short'] must be dict or list[dict]. Recieved {type(side)}") parts = [] - for leg in _iter_side(legs.get("long")): + long = legs.get("long") + short = legs.get("short") + long = sorted(long, key=lambda x: x["opttick"]) if long else [] + short = sorted(short, key=lambda x: x["opttick"]) if short else [] + for leg in _iter_side(long): parts.append(f"&L:{leg['opttick']}") - for leg in _iter_side(legs.get("short")): + for leg in _iter_side(short): parts.append(f"&S:{leg['opttick']}") return "".join(parts) @@ -459,3 +472,28 @@ def extract_order(obj): order["data"]["close"] += mid if direction == "long" else -mid order["data"]["trade_id"] = create_trade_id(pack) return order + +def _order_formatting( + trade_id: str, + legs: List[Tuple[str, str]], + close: float, +) -> Dict[str, Any]: + """ + Formats the order details into a structured dictionary. + + Args: + trade_id (str): Unique identifier for the trade. + legs (List[Tuple[str, str]]): A list of tuples containing the long and short leg option ticks. Eg: [('L', "AAPL230616C00150000"), ('S', "AAPL230616P00150000")] + close (float): The closing price of the order. + """ + order = {} + order["trade_id"] = trade_id + order["close"] = close + for direction, opttick in legs: + if direction.upper() == "L": + order.setdefault("long", []).append(opttick) + elif direction.upper() == "S": + order.setdefault("short", []).append(opttick) + else: + raise ValueError(f"Invalid leg direction: {direction}. Must be 'L' or 'S'.") + return order diff --git a/EventDriven/riskmanager/picker/order_picker.py b/EventDriven/riskmanager/picker/order_picker.py index 3dbe66f..55b2b78 100644 --- a/EventDriven/riskmanager/picker/order_picker.py +++ b/EventDriven/riskmanager/picker/order_picker.py @@ -189,16 +189,19 @@ precompute_lookbacks, ) from EventDriven.configs.core import ChainConfig, OrderSchemaConfigs, OrderPickerConfig, OrderResolutionConfig +from EventDriven.types import ResultsEnum from ..utils import ( dynamic_memoize, + parse_position_id ) from trade.helpers.Logging import setup_logger from trade.helpers.decorators import timeit -from EventDriven.riskmanager.picker import OrderSchema, build_strategy, extract_order +from EventDriven.riskmanager.picker import OrderSchema, build_strategy, extract_order, _order_formatting from EventDriven.dataclasses.orders import OrderRequest from EventDriven.riskmanager._orders import order_resolve_loop, order_failed from EventDriven.types import Order +import numpy as np logger = setup_logger("EventDriven.riskmanager.picker.order_picker") @@ -224,12 +227,59 @@ def __init__(self, start_date: str | datetime, end_date: str | datetime): self._order_schema_config = OrderSchemaConfigs() self._order_resolution_config = OrderResolutionConfig() + ## Others + self.preset_orders = {} + def __repr__(self): return f"OrderPicker(start_date={self.start_date}, end_date={self.end_date})" @property def lookback(self): return self.__lookback + + def register_preset_order(self, + signal_id: str, + trade_id: str, + date: str | datetime, + close_price: float = np.nan): + + """ + Register a preset order to be used instead of generating a new one. + This is useful for backtesting scenarios where specific orders need to be enforced. + """ + self.preset_orders[signal_id] = { + "trade_id": trade_id, + "date": pd.to_datetime(date, format="%Y-%m-%d").date(), + "close_price": close_price + } + + def clear_preset_orders(self): + """ + Clear all registered preset orders. + """ + self.preset_orders = {} + + def get_preset_order(self, signal_id: str, date: str | datetime) -> dict: + """ + Check if a preset order exists for the given signal_id and date + If it exists, return the preset order details; otherwise, return an empty dictionary. + It we will format the order as expected by the rest of the system. + """ + preset_order = self.preset_orders.get(signal_id, None) + if preset_order and preset_order["date"] == pd.to_datetime(date, format="%Y-%m-%d").date(): + _, legs = parse_position_id(preset_order["trade_id"]) + data = _order_formatting( + trade_id=preset_order["trade_id"], + legs=legs, + close=preset_order["close_price"] + ) + return { + "result": ResultsEnum.SUCCESSFUL.value, + "data": data, + "map_signal_id": signal_id, + "signal_id": signal_id, + } + return {} def get_order_schema(self, ticker: str, option_type: str = "P", max_total_price: float = None) -> OrderSchema: """ @@ -285,6 +335,7 @@ def _get_order( """ Get the order for the given schema, date, and spot price. """ + assert isinstance(schema, tuple), "Schema must be a tuple of items." schema = OrderSchema(dict(schema)) if schema["option_type"].lower() == "c": ## This ensures that both call and put OTM are < 1.0 and ITM are > 1.0 @@ -385,31 +436,36 @@ def _get_open_order_backtest( returns: Order: The resolved order object. """ - schema = picker.get_order_schema( - ticker=request.symbol, option_type=request.option_type, max_total_price=request.max_close - ) - schema_as_tuple = tuple(schema.data.items()) - order = picker._get_order( - schema=schema_as_tuple, date=request.date, spot=request.spot, chain_spot=request.chain_spot, print_url=False - ) - - ## Resolve order if failed and resolution is enabled - if picker._order_resolution_config.resolve_enabled: - order = order_resolve_loop( - order=order, - schema=schema, - date=inputs.date, - spot=inputs.spot, - max_close=inputs.tick_cash / 100, ## Use tick cash to determine max close. Normalize to 100 contracts - max_dte_tolerance=inputs.max_dte_tolerance, - max_tries=inputs.max_tries, - otm_moneyness_width=inputs.otm_moneyness_width, - itm_moneyness_width=inputs.itm_moneyness_width, - logger=logger, - signalID=inputs.signal_id, - schema_cache={}, - picker=picker, + order = picker.get_preset_order(signal_id=inputs.signal_id, date=inputs.date) + if not order: + logger.info(f"No preset order found for signal_id {inputs.signal_id} on date {inputs.date}. Generating new order.") + schema = picker.get_order_schema( + ticker=request.symbol, option_type=request.option_type, max_total_price=request.max_close ) + schema_as_tuple = tuple(schema.data.items()) + order = picker._get_order( + schema=schema_as_tuple, date=request.date, spot=request.spot, chain_spot=request.chain_spot, print_url=False + ) + + ## Resolve order if failed and resolution is enabled + if picker._order_resolution_config.resolve_enabled: + order = order_resolve_loop( + order=order, + schema=schema, + date=inputs.date, + spot=inputs.spot, + max_close=inputs.tick_cash / 100, ## Use tick cash to determine max close. Normalize to 100 contracts + max_dte_tolerance=inputs.max_dte_tolerance, + max_tries=inputs.max_tries, + otm_moneyness_width=inputs.otm_moneyness_width, + itm_moneyness_width=inputs.itm_moneyness_width, + logger=logger, + signalID=inputs.signal_id, + schema_cache={}, + picker=picker, + ) + else: + logger.info(f"Preset order found for signal_id {inputs.signal_id} on date {inputs.date}. Using preset order.") ## Add necessary tags for identification order["signal_id"] = inputs.signal_id diff --git a/EventDriven/riskmanager/position/cogs/analyze_utils.py b/EventDriven/riskmanager/position/cogs/analyze_utils.py index c4085c8..7e064a3 100644 --- a/EventDriven/riskmanager/position/cogs/analyze_utils.py +++ b/EventDriven/riskmanager/position/cogs/analyze_utils.py @@ -297,8 +297,12 @@ def greek_check(greek_value: float, greek_threshold: float, qty: int = 1, greate per_greek = greek_value / qty _bool = abs(greek_value) > abs(greek_threshold) logger.info( - f"Greek Check: greek_value={greek_value}, greek_threshold={greek_threshold}, per_greek={per_greek}, _bool={_bool}" + (f"Greek Check: greek_value={greek_value}, greek_threshold={greek_threshold}, per_greek={per_greek}, _bool={_bool}" + f", qty={qty}") ) + if greek_value == 0: + logger.critical("Greek value is zero, cannot compute required quantity. Returning False and 0.") + return False, 0 required_qty = max(int(abs(greek_threshold) // abs(per_greek)), 1) quantity_diff = abs(qty) - abs(required_qty) return _bool, quantity_diff @@ -384,15 +388,44 @@ def analyze_position( if _greek_bool: abs_q_diff = abs(q_diff) + + ## Will use this later when adding short/long direction info to log + direction = "L" if qty > 0 else "S" # noqa + logger.info( + (f"Greek limit breach for {trade_id}: greek={greek}, greek_v={greek_v}, " + f"greek_limit_v={greek_limit_v}, qty={qty}, direction={direction}, " + f"abs_q_diff={abs_q_diff}, q_diff={q_diff}") + ) + # Keep track of the largest adjustment needed if abs_q_diff > max_qty_diff: max_qty_diff = abs_q_diff # Adjust sign based on position direction + # Eg: when LONG=qty>0 we set negative qty_diff to reduce position size + # TODO: q_diff sign logic needs review for situations where we want to increase position size q_diff = abs_q_diff if qty < 0 else -abs_q_diff - max_adjust_action = ADJUST( - trade_id=trade_id, action=Changes(quantity_diff=q_diff, new_quantity=qty + q_diff) - ) - max_adjust_action.reason = f"position {greek} exceeds limit ({greek_v} > {greek_limit_v})" + new_qty = qty + q_diff + + ## IF new quantity is positive, create ADJUST action + if new_qty > 0: + max_adjust_action = ADJUST( + trade_id=trade_id, action=Changes(quantity_diff=q_diff, new_quantity=qty + q_diff) + ) + max_adjust_action.reason = f"position {greek} exceeds limit ({greek_v} > {greek_limit_v})" + + ## IF new quantity is zero, create ROLL action instead. To avoid complete close. + elif new_qty == 0: + max_adjust_action = ROLL( + trade_id=trade_id, action=Changes(quantity_diff=q_diff, new_quantity=0) + ) + max_adjust_action.reason = f"position {greek} exceeds limit ({greek_v} > {greek_limit_v}), New qty=0, rolling instead of closing." + + else: + logger.warning( + (f"Calculated new quantity for {trade_id} is negative ({new_qty}). " + "Skipping ADJUST action creation.") + ) + logger.debug(f"ADJUST action candidate for {trade_id}: {greek} limit breach.") else: logger.debug(f"No {greek} limit breach for {trade_id}: {greek_v} within {greek_limit_v}.") diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/README.md b/EventDriven/riskmanager/position/cogs/benchmarks/README.md deleted file mode 100644 index a86b68e..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# LimitsAndSizingCog Performance Benchmarks - -## Overview - -This directory contains focused benchmarks for measuring `LimitsAndSizingCog._analyze_impl()` performance. - -## Files - -- **`simple_focused_benchmark.py`** - Main benchmark script -- **`simple_focused_baseline.json`** - Baseline performance results (200 iterations, 50 positions) -- **`task4_results.json`** - Results after conditional verbose_info optimization -- **`task5_results.json`** - Results after pre-compute values optimization -- **`task3_final.json`** - Results for dictionary filtering (inconclusive) - -## Usage - -```bash -# Run current performance test -conda run -n openbb_new_use python simple_focused_benchmark.py --iterations 200 --positions 50 - -# Create new baseline -python simple_focused_benchmark.py --baseline --iterations 200 --positions 50 - -# Custom output -python simple_focused_benchmark.py --output my_test.json --iterations 200 --positions 50 -``` - ---- - -## Optimization Results Summary - -### Performance Metrics - -| Metric | Baseline | After Task #4 | After Task #5 | Change | -|--------|----------|---------------|---------------|---------| -| **Time/iteration** | 0.160971s | 0.081332s | 0.094664s | **-41.19%** | -| **Time/position** | 3.219ms | 1.627ms | 1.893ms | **-41.19%** | -| **Variance (CV)** | 123.40% | 50.20% | 45.34% | **-63.23%** | - -🚀 **Overall Speedup: 41.19% faster** -✅ **Stability: 63% reduction in variance** -✅ **Per-position: 1.326ms faster (3.219ms → 1.893ms)** - ---- - -## Completed Optimizations - -### ✅ Task #1: Combined Redundant Parsing Functions -- Created `get_dte_and_moneyness_from_trade_id()` function -- Eliminates redundant `parse_position_id()` calls -- Status: Already implemented in codebase - -### ✅ Task #2: Early Returns in analyze_position() -- Implemented priority-ordered returns (EXERCISE → ROLL → ADJUST → HOLD) -- Eliminated list building and sorting overhead -- Status: Already implemented in codebase - -### ⚠️ Task #3: Dictionary Filtering Optimization -- Changed from O(n) to O(1) lookup using MEASURES_SET -- Result: Inconclusive (within statistical noise, CV >120%) -- Status: Implemented but minimal practical impact - -### ✅ Task #4: Conditional verbose_info Generation ⭐ **MAJOR WIN** -- Only generate verbose_info for non-HOLD actions -- Changed from multi-line to single-line f-string format -- Result: **49.47% speedup**, CV reduced to 50.20% -- Status: Implemented - -### ✅ Task #5: Pre-compute Commonly Used Values -- Pre-computed: strat_enabled_limits, bkt_start_date, t_plus_n, last_updated, t_plus_n_timedelta -- Result: **41.19% faster than baseline** (16% regression vs Task #4) -- Status: Implemented - -### ✅ Task #6: Cache Config Lookup Outside Loop -- Status: Completed as part of Task #5 - ---- - -## Key Insights - -1. **Task #4 was the game changer** - Skipping string formatting for HOLD actions provided biggest impact -2. **Variance reduction significant** - CV dropped from 123% to 45%, more consistent performance -3. **Pre-computation trade-offs** - Task #5 adds small overhead but improves code clarity -4. **Statistical challenges** - High baseline variance makes small optimizations (<10%) hard to measure -5. **Real bottleneck elsewhere** - Greeks calculation in market_timeseries.py (~5 min/position) remains main issue - ---- - -## Recommendations - -- ✅ Keep all current optimizations (improve both performance and code quality) -- ✅ Task #4 alone provides 49.47% speedup if cherry-picking needed -- 🔍 Focus future efforts on Greeks calculation bottleneck -- 📊 Target >10% improvements for measurable results diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_baseline.json b/EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_baseline.json deleted file mode 100644 index 1776938..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_baseline.json +++ /dev/null @@ -1,214 +0,0 @@ -{ - "timestamp": "2025-11-28T21:33:45.634871", - "iterations": 200, - "num_positions": 50, - "timings_seconds": [ - 0.1124361170004704, - 0.09299577799902181, - 0.11549446400022134, - 0.12602316199991037, - 0.11676277600054163, - 0.12863105799988261, - 0.09120676099701086, - 0.09050670400029048, - 0.08694563400058541, - 0.10048900400215643, - 0.11537297299946658, - 0.10663737800132367, - 0.08386945299935178, - 0.07379269000011845, - 0.06282298999940394, - 0.06990236999990884, - 0.09860984999977518, - 0.07444966700131772, - 0.06107574500128976, - 0.10163106899926788, - 0.10475266800131067, - 0.10018179300095653, - 0.1025254349988245, - 0.09240633500303375, - 0.07005534000199987, - 0.0876700320004602, - 0.08804916400185903, - 0.09102904199971817, - 0.2022440020009526, - 0.12140582199936034, - 0.08622879999893485, - 0.09916428799988353, - 0.11940119199789478, - 0.07631051499993191, - 0.06977537800048594, - 0.13554363200091757, - 0.10836319000009098, - 0.0904391999974905, - 0.11126822000005632, - 0.09532266099995468, - 0.0987091099996178, - 0.10418092000327306, - 0.10626124199916376, - 0.08507717400061665, - 0.06438516699927277, - 0.062934588000644, - 0.07466716899944004, - 0.09065367600123864, - 0.06420887000058428, - 0.06638465099968016, - 0.12703987799977767, - 0.12465908900048817, - 0.1227337609998358, - 0.21896897799888393, - 0.14963359200191917, - 0.1052738340003998, - 0.11840194700198481, - 0.097849508998479, - 0.1304657140026393, - 0.12750545800008695, - 0.09471298499920522, - 0.09001788100067643, - 0.09015447400088306, - 0.09733147299994016, - 0.12127994000184117, - 0.07530326000050991, - 0.07572819699998945, - 0.06424139499722514, - 0.09457172799739055, - 0.10890259699954186, - 0.0889476789998298, - 0.08417465800084756, - 0.08644267600175226, - 0.131837386998086, - 0.09230055299849482, - 0.09447187099794974, - 0.08371949999855133, - 0.09390294900003937, - 0.1242689339997014, - 0.09814644699872588, - 0.06983653899806086, - 0.07643277100214618, - 0.07760205199883785, - 0.07555404499726137, - 0.07501138400039054, - 0.0806162429980759, - 0.06475468299686327, - 0.060695856001984794, - 0.07742830399729428, - 0.06296733400085941, - 0.06614110300142784, - 0.07171886200012523, - 0.06688193900117767, - 0.07119920600234764, - 0.07123606700042728, - 0.08730313599880901, - 0.09560939299990423, - 0.06870151000111946, - 0.10333523400186095, - 0.10427913900275598, - 0.08983682500183932, - 0.08754351899915491, - 0.12119196500134422, - 1.286703485999169, - 0.18888503199923434, - 0.9170801389991539, - 0.9593940779996046, - 0.6825994479986548, - 0.6668549070018344, - 1.8159945869992953, - 0.8891261879980448, - 0.611302320001414, - 0.5783604610005568, - 0.5785957989974122, - 0.39401062999968417, - 0.4140847880007641, - 0.31359697000152664, - 0.17436419099976774, - 0.19653211499826284, - 0.24810775399964768, - 0.17058249399997294, - 0.15014555000016117, - 0.13030647200139356, - 0.24010779400123283, - 0.1366094080003677, - 0.1198702889996639, - 0.15558540799975162, - 0.16952530499838758, - 0.11712136599817313, - 0.10913464599798317, - 0.09775316500235931, - 0.09113733199774288, - 0.09767447500053095, - 0.18424123600198072, - 0.12506186199971125, - 0.1352459590016224, - 0.19999694500074838, - 0.2969247950022691, - 0.11984094699801062, - 0.11258214900226449, - 0.19739717400079826, - 0.2687673990003532, - 0.19013910799912992, - 0.3976287229997979, - 0.3565994119999232, - 0.2396827660013514, - 0.2728527949984709, - 0.17461650900077075, - 0.1295863660016039, - 0.17390844100009417, - 0.2040776760004519, - 0.21835745900170878, - 0.24066108300030464, - 0.16474368900162517, - 0.14774868599852198, - 0.16687833900141413, - 0.11534100099743227, - 0.1146964699983073, - 0.09377230300015071, - 0.10763487000076566, - 0.09638726200137171, - 0.10305503599738586, - 0.10307674199793837, - 0.11019019500236027, - 0.10518714799763984, - 0.10919626299801166, - 0.12154040599853033, - 0.11554302000149619, - 0.11421647800307255, - 0.10381807999874582, - 0.12099867000142694, - 0.2382874590002757, - 0.2028360780022922, - 0.28585212500183843, - 0.10810091399980593, - 0.1021537749984418, - 0.1141872840016731, - 0.11561524200078566, - 0.08132935100002214, - 0.10436057000333676, - 0.09592388899909565, - 0.10983376199874328, - 0.09385990600276273, - 0.09405442800198216, - 0.12945240099725197, - 0.15149066500089248, - 0.11215841999728582, - 0.10884799199993722, - 0.08932809500038275, - 0.07133894399885321, - 0.09206479400017997, - 0.08779134099677322, - 0.08392427599756047, - 0.09182500899987645, - 0.09363464000125532, - 0.10426684900085093, - 0.07835840500047198, - 0.08563552700070431, - 0.09745183000268298, - 0.09877254399907542 - ], - "avg_time_seconds": 0.16097098473006555, - "min_time_seconds": 0.060695856001984794, - "max_time_seconds": 1.8159945869992953, - "avg_time_per_position_ms": 3.219419694601311, - "total_time_seconds": 32.19419694601311, - "version": "baseline", - "description": "Baseline before Task #2 (early returns optimization)" -} \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_benchmark.py b/EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_benchmark.py deleted file mode 100644 index 0d1c114..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_benchmark.py +++ /dev/null @@ -1,339 +0,0 @@ -""" -Simple focused benchmark for LimitsAndSizingCog._analyze_impl() - -Directly creates sample position contexts and benchmarks the analyze method. -Based on the notebook approach in positions_and_limits_focus.ipynb Cell 21. - -Usage: - cd /path/to/QuantTools - python EventDriven/riskmanager/position/cogs/benchmarks/simple_focused_benchmark.py --baseline --iterations 100 -""" - -from __future__ import annotations - -import time -import json -import argparse -from pathlib import Path -from datetime import datetime -from typing import Dict, Any -import pandas as pd -import sys - -# Ensure proper imports -sys.path.insert(0, '/Users/chiemelienwanisobi/cloned_repos/QuantTools') - -from EventDriven.riskmanager.position.cogs.limits import LimitsAndSizingCog -from EventDriven.configs.core import LimitsEnabledConfig -from EventDriven.dataclasses.limits import PositionLimits - - -def create_sample_positions_for_benchmark(num_positions: int = 10): - """ - Create sample position data for benchmarking. - Returns a list of mock position objects that _analyze_impl would process. - """ - from EventDriven.dataclasses.states import PositionState, PortfolioState, PortfolioMetaInfo, PositionAnalysisContext - from EventDriven.dataclasses.timeseries import AtTimePositionData - from EventDriven.riskmanager.market_data import AtIndexResult - import pandas as pd - from datetime import datetime, timedelta - - print(f"Creating {num_positions} sample positions...") - - positions = [] - base_date = datetime(2024, 6, 14) - - for i in range(num_positions): - # Create trade_id with proper format - # Format: &L:SYMBOL_YYYYMMDD_C/P_STRIKE&S:SYMBOL_YYYYMMDD_C/P_STRIKE - expiry = (base_date + timedelta(days=180+i)).strftime("%Y%m%d") - strike1 = int(150 + i * 5) - strike2 = int(strike1 + 10) - trade_id = f"&L:AAPL{expiry}C{strike1}&S:AAPL{expiry}C{strike2}" - - # Create mock position data (AtTimePositionData) - position_data = AtTimePositionData( - position_id=trade_id, - date=base_date + timedelta(days=i), - close=10.5 + i, - bid=10.0 + i, - ask=11.0 + i, - midpoint=10.5 + i, - delta=0.45 + (i * 0.05), # Vary delta - gamma=0.02, - theta=-0.15, - vega=0.25, - ) - - # Create mock underlier data - undl_price = 150.0 + i * 2 - underlier_data = AtIndexResult( - sym="AAPL", - date=pd.Timestamp(base_date + timedelta(days=i)), - spot=pd.Series([undl_price], index=[base_date + timedelta(days=i)]), - chain_spot=pd.Series({"close": undl_price, "open": undl_price-1, "high": undl_price+1, "low": undl_price-2}), - rates=pd.Series([0.05], index=[base_date + timedelta(days=i)]), - dividends=pd.Series([0.0], index=[base_date + timedelta(days=i)]) - ) - - # Create position - position = PositionState( - trade_id=trade_id, - signal_id=f"SIGNAL_{i}", - underlier_tick="AAPL", - quantity=10, - entry_price=10.0, - current_position_data=position_data, - current_underlier_data=underlier_data, - pnl=100.0 + i * 10, - last_updated=base_date + timedelta(days=i), - ) - - positions.append(position) - - # Create portfolio state - portfolio = PortfolioState( - cash=100000.0, - positions=positions, - pnl=sum([p.pnl for p in positions]), - total_value=100000.0 + sum([p.pnl for p in positions]), - last_updated=base_date, - ) - - # Create portfolio meta info - portfolio_meta = PortfolioMetaInfo( - start_date=base_date - timedelta(days=30), - t_plus_n=0, - ) - - # Create context - context = PositionAnalysisContext( - date=base_date, - portfolio=portfolio, - portfolio_meta=portfolio_meta, - ) - - return context - - -def setup_cog_with_limits(context): - """ - Create a LimitsAndSizingCog and initialize position_limits. - Similar to notebook Cell 21 approach. - - Also patches database lookup functions to avoid errors with mock data. - """ - print("Setting up LimitsAndSizingCog with position limits...") - - # Patch adjust_for_events to skip database lookups - from EventDriven.riskmanager.position.cogs import analyze_utils - original_adjust = analyze_utils.adjust_for_events - - def mock_adjust_for_events(start, date, option): - """Mock version that just returns the option unchanged""" - return option - - analyze_utils.adjust_for_events = mock_adjust_for_events - - # Create cog - config = LimitsEnabledConfig() - cog = LimitsAndSizingCog(config=config) - - # Initialize position limits for all positions - for position in context.portfolio.positions: - cog.position_limits[position.trade_id] = PositionLimits( - delta=500.0, # Sample delta limit - dte=120, - moneyness=1.15, - ) - - print(f"Cog initialized with {len(cog.position_limits)} position limits") - return cog - - -def benchmark_analyze_impl( - cog, # LimitsAndSizingCog - context, # PositionAnalysisContext - iterations: int = 100 -) -> Dict[str, Any]: - """ - Benchmark cog._analyze_impl() by calling it repeatedly. - - Args: - cog: Fully initialized LimitsAndSizingCog - context: PositionAnalysisContext with sample positions - iterations: Number of times to call _analyze_impl - - Returns: - Dictionary with timing results - """ - num_positions = len(context.portfolio.positions) - - print(f"\n{'='*70}") - print("FOCUSED BENCHMARK: LimitsAndSizingCog._analyze_impl()") - print(f"Positions: {num_positions}, Iterations: {iterations}") - print(f"{'='*70}\n") - - timings = [] - - print("Starting benchmark...") - - for i in range(iterations): - iteration_start = time.perf_counter() - - # Run _analyze_impl - _ = cog._analyze_impl(context) - - iteration_time = time.perf_counter() - iteration_start - timings.append(iteration_time) - - # Print progress every 20 iterations - if (i + 1) % 20 == 0: - avg_so_far = sum(timings) / len(timings) - print(f"Iteration {i+1}/{iterations}: {iteration_time:.6f}s " - f"(avg so far: {avg_so_far:.6f}s)") - - # Calculate statistics - avg_time = sum(timings) / len(timings) - min_time = min(timings) - max_time = max(timings) - - # Time per position - avg_time_per_position_ms = (avg_time / num_positions * 1000) if num_positions > 0 else 0 - - results = { - 'timestamp': datetime.now().isoformat(), - 'iterations': iterations, - 'num_positions': num_positions, - 'timings_seconds': timings, - 'avg_time_seconds': avg_time, - 'min_time_seconds': min_time, - 'max_time_seconds': max_time, - 'avg_time_per_position_ms': avg_time_per_position_ms, - 'total_time_seconds': sum(timings), - } - - print(f"\n{'='*70}") - print("RESULTS:") - print(f" Average time per iteration: {avg_time:.6f}s") - print(f" Min time: {min_time:.6f}s") - print(f" Max time: {max_time:.6f}s") - print(f" Avg time per position: {avg_time_per_position_ms:.3f}ms") - print(f" Total positions analyzed: {num_positions * iterations:,}") - print(f"{'='*70}\n") - - return results - - -def compare_with_baseline(current_results: Dict[str, Any], baseline_file: Path): - """Compare current results with baseline.""" - if not baseline_file.exists(): - print("No baseline file found for comparison") - return - - with open(baseline_file, 'r') as f: - baseline = json.load(f) - - baseline_avg = baseline['avg_time_seconds'] - current_avg = current_results['avg_time_seconds'] - - time_diff = current_avg - baseline_avg - pct_change = (time_diff / baseline_avg) * 100 - - baseline_per_pos = baseline.get('avg_time_per_position_ms', 0) - current_per_pos = current_results['avg_time_per_position_ms'] - - print(f"{'='*70}") - print("COMPARISON WITH BASELINE:") - print(f" Baseline avg time: {baseline_avg:.6f}s") - print(f" Current avg time: {current_avg:.6f}s") - print(f" Time difference: {time_diff:+.6f}s ({pct_change:+.2f}%)") - print("") - print(f" Baseline per position: {baseline_per_pos:.3f}ms") - print(f" Current per position: {current_per_pos:.3f}ms") - - if pct_change < 0: - speedup = -pct_change - print("") - print(f" 🚀 SPEEDUP: {speedup:.2f}% faster!") - elif pct_change > 0: - print("") - print(f" ⚠️ REGRESSION: {pct_change:.2f}% slower") - else: - print("") - print(" No change") - - print(f"{'='*70}\n") - - -def main(): - parser = argparse.ArgumentParser( - description='Simple focused benchmark for LimitsAndSizingCog._analyze_impl()' - ) - parser.add_argument( - '--baseline', - action='store_true', - help='Save results as baseline for future comparisons' - ) - parser.add_argument( - '--iterations', - type=int, - default=100, - help='Number of iterations (default: 100)' - ) - parser.add_argument( - '--positions', - type=int, - default=10, - help='Number of sample positions to create (default: 10)' - ) - parser.add_argument( - '--output', - type=str, - help='Custom output filename (default: auto-generated)' - ) - - args = parser.parse_args() - - # Create sample data - print("="*70) - print("SETUP PHASE") - print("="*70) - - context = create_sample_positions_for_benchmark(num_positions=args.positions) - cog = setup_cog_with_limits(context) - - # Run benchmark - results = benchmark_analyze_impl(cog, context, iterations=args.iterations) - - # Determine output file - benchmarks_dir = Path(__file__).resolve().parent - - if args.output: - output_file = benchmarks_dir / args.output - elif args.baseline: - output_file = benchmarks_dir / 'simple_focused_baseline.json' - results['version'] = 'baseline' - results['description'] = 'Baseline before Task #2 (early returns optimization)' - else: - # Auto-generate filename with timestamp - timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') - output_file = benchmarks_dir / f'simple_focused_results_{timestamp}.json' - results['version'] = 'current' - results['description'] = 'Current performance measurement' - - # Save results - with open(output_file, 'w') as f: - json.dump(results, f, indent=2) - - print(f"Results saved to: {output_file}") - - # Compare with baseline if not creating baseline - if not args.baseline: - baseline_file = benchmarks_dir / 'simple_focused_baseline.json' - compare_with_baseline(results, baseline_file) - - -if __name__ == '__main__': - main() diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/task3_final.json b/EventDriven/riskmanager/position/cogs/benchmarks/task3_final.json deleted file mode 100644 index f15a453..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/task3_final.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "timestamp": "2025-11-28T21:32:17.270703", - "iterations": 200, - "num_positions": 50, - "timings_seconds": [ - 0.10376109600110794, - 0.09935514500102727, - 0.09825622099742759, - 0.11950418599735713, - 0.24454587700165575, - 0.14475858399964636, - 0.1213206540014653, - 0.1495928990007087, - 0.23858742200172856, - 0.13508260600065114, - 0.1287371529979282, - 0.13199195000197506, - 0.1185176170001796, - 0.27653049700165866, - 0.1428144900019106, - 0.10033040300186258, - 0.12397298900032183, - 0.16280647899839096, - 0.12326351999945473, - 0.10962116299924674, - 0.09665007800140302, - 0.10309922799933702, - 0.09308468600283959, - 0.12122351099969819, - 0.12120716199933668, - 0.12005923000106122, - 0.1140902119987004, - 0.10927746599918464, - 0.09675935600171215, - 0.14102302799801691, - 0.09598366499994881, - 0.1026014019989816, - 1.4717046519981523, - 0.23798462500053574, - 0.3141331970000465, - 0.5197073290000844, - 0.26338066599782906, - 0.08705067299888469, - 0.09597225499965134, - 0.10384067899940419, - 0.1372531750021153, - 0.3232995779981138, - 0.24534615400261828, - 0.15478637700289255, - 0.11982172399802948, - 0.10955134599862504, - 0.20797378399947775, - 1.3797600260004401, - 1.4902170330024092, - 0.6410117090017593, - 0.26603874400097993, - 0.7129434809976374, - 1.3723017080010322, - 1.0471812249998038, - 0.27059267300137435, - 0.13780781599780312, - 0.6493040589994052, - 0.27505965499949525, - 0.23714444399956847, - 0.5072346180022578, - 0.3147437000006903, - 0.372903760002373, - 0.19658536099814228, - 0.13626854899848695, - 0.10973763799847802, - 0.2955967369998689, - 0.1897965429998294, - 0.12465695700302604, - 0.14000731499982066, - 0.09953991400107043, - 0.09272833700015326, - 0.12514273399938247, - 0.10770355500062578, - 0.1101387570015504, - 0.10079654500077595, - 0.08130877599978703, - 0.09514594299980672, - 0.10921632199824671, - 0.09533140500207082, - 0.08822215399777633, - 0.08312510500036296, - 0.10378292099994724, - 0.09116379200349911, - 0.08495508600026369, - 0.09924015199794667, - 0.0981659250028315, - 0.10462745300173992, - 0.1046998279998661, - 0.12148934700235259, - 0.11892350599737256, - 0.09046813899840345, - 0.07243784000093001, - 0.08513366800252697, - 0.09248968199972296, - 0.0912349419995735, - 0.09260913999969489, - 0.08783014400250977, - 0.10365281799749937, - 0.10020152499782853, - 0.09935832200062578, - 0.09856337000019266, - 0.1719422169990139, - 0.34931337099988014, - 0.11664032000044244, - 0.122843664001266, - 0.13118475399824092, - 0.12130267599786748, - 0.2489404630032368, - 0.12321556099777808, - 0.08098265300213825, - 0.1117831350020424, - 0.09261805899950559, - 0.10812797300241073, - 0.11664468800154282, - 0.11336259900053847, - 0.2437336970033357, - 0.1610078359990439, - 0.10581661899777828, - 0.11578824599928339, - 0.10640758400040795, - 0.13030005499967956, - 0.19003671099926578, - 0.11208337399875745, - 0.15721414899962838, - 0.10919898800057126, - 0.14454675099841552, - 0.11043093500120449, - 0.1327641659991059, - 0.1071581310025067, - 0.10791002800033311, - 0.09702216100049554, - 0.1031249250008841, - 0.10566747700067936, - 0.1407305409993569, - 0.10028188900105306, - 0.13989794300141511, - 0.15907919599703746, - 0.13067604699972435, - 0.1322535599974799, - 0.15618954000092344, - 0.13827253600175027, - 0.15345564899689634, - 0.10380831799920998, - 0.10692864899829146, - 0.14816435100146919, - 0.16982753299816977, - 0.14006941600018763, - 0.10380725199865992, - 0.10335612500057323, - 0.10608995000075083, - 0.10322661000100197, - 0.10016873499989742, - 0.11727481699927012, - 0.09293408499797806, - 0.08196283700090135, - 0.08167696300006355, - 0.07503288600128144, - 0.08688244800214306, - 0.07652016200154321, - 0.17649227900255937, - 0.09538924599837628, - 0.07221700300215161, - 0.08366980700157, - 0.07416483699853416, - 0.072810894002032, - 0.06579944300028728, - 0.07651960699877236, - 0.09162528500019107, - 0.09562968600221211, - 0.10555635700075072, - 0.06667220100280247, - 0.07085439199727261, - 0.08183557400116115, - 0.07097276299828081, - 0.07493035699735628, - 0.07534727000165731, - 0.07236304300022312, - 0.06745456399949035, - 0.07822888499867986, - 0.0798325969990401, - 0.09143882999705966, - 0.08929445800094982, - 0.07670267999856151, - 0.0800732310017338, - 0.07943687199804117, - 0.09144498199748341, - 0.07622820300093736, - 0.08617419200163567, - 0.08993705499960924, - 0.07522768700073357, - 0.08157150400074897, - 0.07382942100230139, - 0.08233769800062873, - 0.0663328080008796, - 0.08509177199812257, - 0.07105297799716936, - 0.0857515890020295, - 0.09671582699957071, - 0.08762320199821261, - 0.08726787700288696 - ], - "avg_time_seconds": 0.16688059023510504, - "min_time_seconds": 0.06579944300028728, - "max_time_seconds": 1.4902170330024092, - "avg_time_per_position_ms": 3.337611804702101, - "total_time_seconds": 33.37611804702101 -} \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/task3_results.json b/EventDriven/riskmanager/position/cogs/benchmarks/task3_results.json deleted file mode 100644 index 7546969..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/task3_results.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "timestamp": "2025-11-28T21:29:08.327421", - "iterations": 100, - "num_positions": 20, - "timings_seconds": [ - 0.025683709998702398, - 0.020152925000729738, - 0.02320113899986609, - 0.03706265399887343, - 0.06792625200250768, - 0.03622309099955601, - 0.04045523099921411, - 0.053271845001290785, - 0.03145509100068011, - 0.03947335499833571, - 0.02823916000124882, - 0.03880956600187346, - 0.033396122002159245, - 0.027862807000929024, - 0.02400205000230926, - 0.025664060998678906, - 0.034076838001055876, - 0.03719533600087743, - 0.0411442540025746, - 0.045254387998284074, - 0.0479930580004293, - 0.044116168002801714, - 0.0240333859983366, - 0.023890268999821274, - 0.03298669300056645, - 0.02315902400005143, - 0.12158002000069246, - 0.03565643399997498, - 0.046293268998852, - 0.03612355400036904, - 0.03792614299891284, - 0.023702451999270124, - 0.021638481000991305, - 0.023517360001278576, - 0.02035377399806748, - 0.02232984899819712, - 0.02174592400115216, - 0.022809539001173107, - 0.02045805500165443, - 0.01915215600092779, - 0.01916415700179641, - 0.01863724299983005, - 0.018414864996884717, - 0.020810232999792788, - 0.019779641002969583, - 0.019439333002083004, - 0.018076349999319063, - 0.01809049999792478, - 0.018625878998136614, - 0.02102698000089731, - 0.02019918800215237, - 0.018796274001942948, - 0.020732822998979827, - 0.018559821000962984, - 0.018934856001578737, - 0.020470247000048403, - 0.031346239000413334, - 0.02675720999832265, - 0.03630095100015751, - 0.02663210099854041, - 0.020028512997669168, - 0.022513693998917006, - 0.020755031000589952, - 0.018753364001895534, - 0.020271237000997644, - 0.017838200001278892, - 0.018975923001562478, - 0.018255287999636494, - 0.01925110900265281, - 0.020470078001380898, - 0.019190119000995765, - 0.01822485899901949, - 0.017907649002154358, - 0.01810339300209307, - 0.018424399000650737, - 0.019848415999149438, - 0.021933446998446016, - 0.01884492400131421, - 0.018331057999603217, - 0.02914421700188541, - 0.035858844999893336, - 0.024564138999267016, - 0.02347419099896797, - 0.02015025000218884, - 0.018801563001034083, - 0.0187240519990155, - 0.019705180002347333, - 0.019342243002029136, - 0.018497717999707675, - 0.01805099900229834, - 0.01796581599774072, - 0.019439892999798758, - 0.019454487999610137, - 0.018129798998415936, - 0.01904448700224748, - 0.019971674999396782, - 0.018313955999474274, - 0.017905429998791078, - 0.018326125002204208, - 0.01802268100072979 - ], - "avg_time_seconds": 0.026116188220330513, - "min_time_seconds": 0.017838200001278892, - "max_time_seconds": 0.12158002000069246, - "avg_time_per_position_ms": 1.3058094110165257, - "total_time_seconds": 2.6116188220330514 -} \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/task3_v2_results.json b/EventDriven/riskmanager/position/cogs/benchmarks/task3_v2_results.json deleted file mode 100644 index 2df506c..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/task3_v2_results.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "timestamp": "2025-11-28T21:29:52.080942", - "iterations": 100, - "num_positions": 20, - "timings_seconds": [ - 0.021662754003045848, - 0.01974582000184455, - 0.018991218999872217, - 0.0191501959998277, - 0.020475552999414504, - 0.022344189001159975, - 0.01878710399978445, - 0.018869308001740137, - 0.019057666999287903, - 0.019546380997780943, - 0.01892279100138694, - 0.01922853499854682, - 0.019272677000117255, - 0.019101048997981707, - 0.020392600003106054, - 0.01930121600162238, - 0.018602524000016274, - 0.018355485000938643, - 0.019618725000327686, - 0.01984880899908603, - 0.018753926000499632, - 0.018649798999831546, - 0.01881892399978824, - 0.01888102700104355, - 0.019449044000793947, - 0.0187890739980503, - 0.018382702000963036, - 0.018554239999502897, - 0.01882993199978955, - 0.019535004001227207, - 0.018878502000006847, - 0.019269989999884274, - 0.021125422998011345, - 0.018393431000731653, - 0.018821082001522882, - 0.018614052001794335, - 0.0201227760007896, - 0.018920673999673454, - 0.020331772000645287, - 0.0197912229996291, - 0.018913938001787756, - 0.0185234529999434, - 0.01934505899771466, - 0.019533278999006143, - 0.019296209000458475, - 0.018608148999192053, - 0.01916171799894073, - 0.018466994999471353, - 0.018763422001939034, - 0.01955843199903029, - 0.018397158000880154, - 0.018582854001579108, - 0.01955319799890276, - 0.019213436000427464, - 0.018664512997929705, - 0.01877710899861995, - 0.01915154599919333, - 0.01964285099893459, - 0.023026265000225976, - 0.021124869002960622, - 0.02166473900069832, - 0.021631260999129154, - 0.022367167999618687, - 0.024521833001927007, - 0.020646360000682762, - 0.021433338999486296, - 0.019395069997699466, - 0.01948952399834525, - 0.01891808200161904, - 0.019834593000268796, - 0.018905955999798607, - 0.018955781000840943, - 0.019910693998099305, - 0.02374935599800665, - 0.019017057999008102, - 0.01860948900139192, - 0.018810670997481793, - 0.018692132998694433, - 0.020318702998338267, - 0.021880585001781583, - 0.02253224099695217, - 0.022535203999723308, - 0.02210570800161804, - 0.020392521997564472, - 0.02002268500291393, - 0.021035374000348384, - 0.01847949000148219, - 0.01958673099943553, - 0.02216648000103305, - 0.02027605399780441, - 0.02031566600271617, - 0.018676078998396406, - 0.019389955999940867, - 0.01919294199979049, - 0.018959172997710994, - 0.019101800997304963, - 0.019802482001978206, - 0.018715131001954433, - 0.018600745999719948, - 0.019190229999367148 - ], - "avg_time_seconds": 0.019703207419988756, - "min_time_seconds": 0.018355485000938643, - "max_time_seconds": 0.024521833001927007, - "avg_time_per_position_ms": 0.9851603709994379, - "total_time_seconds": 1.9703207419988757 -} \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/task4_results.json b/EventDriven/riskmanager/position/cogs/benchmarks/task4_results.json deleted file mode 100644 index 5d71b1a..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/task4_results.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "timestamp": "2025-11-28T21:38:48.390324", - "iterations": 200, - "num_positions": 50, - "timings_seconds": [ - 0.07172146499942755, - 0.06590209700152627, - 0.06444520199875114, - 0.06064467599935597, - 0.061371885996777564, - 0.06423492099929717, - 0.049894014999154024, - 0.04722983099782141, - 0.04805940299775102, - 0.05374076100270031, - 0.06049712800086127, - 0.0488723630005552, - 0.049903275998076424, - 0.05098065800120821, - 0.05525366499932716, - 0.06269634099953691, - 0.055023683002218604, - 0.055815016999986256, - 0.046746367999730865, - 0.06034089300010237, - 0.06989659100145218, - 0.07448034000117332, - 0.05203320899818209, - 0.05111461900014547, - 0.049882082999829436, - 0.04921963499873527, - 0.050692734999756794, - 0.046587251003074925, - 0.060285505998763256, - 0.05677065700001549, - 0.05051560699939728, - 0.04778304600040428, - 0.0460383110003022, - 0.06136006599990651, - 0.06377546899966546, - 0.05713210999965668, - 0.05295515800025896, - 0.04730386599840131, - 0.057402937000006204, - 0.04887588599740411, - 0.052751592000277014, - 0.04575369300073362, - 0.04389704500135849, - 0.04645611399973859, - 0.04571736499929102, - 0.04809168499923544, - 0.04546138600198901, - 0.0451177299983101, - 0.048266565001540584, - 0.0453898730011133, - 0.04460144900076557, - 0.04498080899793422, - 0.045335399998293724, - 0.04506495199893834, - 0.045516886999394046, - 0.055838520001998404, - 0.07144794399937382, - 0.07067959499909193, - 0.050716533998638624, - 0.046691101997566875, - 0.1987632990021666, - 0.134352802997455, - 0.08342028800325352, - 0.06601306599986856, - 0.05955152200112934, - 0.09284645300067496, - 0.08605848099978175, - 0.11455002000002423, - 0.08041388399942662, - 0.0892367730011756, - 0.10368282700073905, - 0.07771267999851261, - 0.10162798600140377, - 0.14279172999886214, - 0.13522267199732596, - 0.05348434999905294, - 0.05108526699768845, - 0.053668939999624854, - 0.05768601599993417, - 0.06954417000088142, - 0.09756778800147003, - 0.058292868998250924, - 0.04582488199957879, - 0.046295031999761704, - 0.12167964500258677, - 0.1442477059972589, - 0.15423888800069108, - 0.33274306999737746, - 0.21234133200050564, - 0.18772949400226935, - 0.16548986799898557, - 0.18396841100184247, - 0.1882081930016284, - 0.13881661199775408, - 0.1254457770010049, - 0.15824151299966616, - 0.1691548819981108, - 0.17370104599831393, - 0.10382883299826062, - 0.1271276530023897, - 0.12175794199720258, - 0.10012779899989255, - 0.13450379999994766, - 0.11192291900078999, - 0.11984474100245279, - 0.11698989299839013, - 0.13776090999817825, - 0.128690778001328, - 0.11472958699960145, - 0.14307594199999585, - 0.09358520800014958, - 0.09910867600046913, - 0.09021141099947272, - 0.06593758599774446, - 0.0678532140009338, - 0.05467090700039989, - 0.07234033699933207, - 0.057982042999356054, - 0.059951077000732766, - 0.05560432800120907, - 0.06293091500265291, - 0.05611050899824477, - 0.05725599000288639, - 0.07053511499907472, - 0.060869134998938534, - 0.062050198001088575, - 0.06059276700034388, - 0.06081721100053983, - 0.05734942799972487, - 0.05945832900033565, - 0.06860756199967, - 0.07246851599848014, - 0.07689762800146127, - 0.06130992999896989, - 0.06991490099971998, - 0.057173518998752115, - 0.06522982000024058, - 0.05630133299928275, - 0.06872772499991697, - 0.06276476199855097, - 0.06393379700239166, - 0.08132730999932392, - 0.05510298499939381, - 0.05682307499955641, - 0.05910154500088538, - 0.10593643500033068, - 0.11815760099852923, - 0.09483234499930404, - 0.12846658600028604, - 0.09659131400258048, - 0.09501996000108193, - 0.0954250350005168, - 0.09966654399977415, - 0.09227550299692666, - 0.08630111800084705, - 0.0975027400018007, - 0.0669736309973814, - 0.25219274300252437, - 0.17760137900040718, - 0.09352386600221507, - 0.09120794000045862, - 0.06910034500106121, - 0.09869582499959506, - 0.09936388899950543, - 0.08486230000198702, - 0.0649020439996093, - 0.0838134590012487, - 0.06431156400140026, - 0.10876256499977899, - 0.07613403999857837, - 0.08114380899860407, - 0.07030104400109849, - 0.06638048700187937, - 0.06496776199855958, - 0.058256964999600314, - 0.07466861200009589, - 0.06877180200172006, - 0.09278434900261345, - 0.09456835400123964, - 0.07485860099768615, - 0.05071483899882878, - 0.046172893002221826, - 0.06137925000075484, - 0.05588075500054401, - 0.06728630299767246, - 0.058633177999581676, - 0.060626251997746294, - 0.0708806309994543, - 0.07586940200053505, - 0.06528588400033186, - 0.082324538998364, - 0.0747122409993608, - 0.08613871400302742, - 0.09409738199974527, - 0.07368804499856196, - 0.058481282998400275, - 0.06898171899956651, - 0.07905184799892595, - 0.11491965500317747, - 0.10068796399718849 - ], - "avg_time_seconds": 0.08133175063992894, - "min_time_seconds": 0.04389704500135849, - "max_time_seconds": 0.33274306999737746, - "avg_time_per_position_ms": 1.6266350127985787, - "total_time_seconds": 16.26635012798579 -} \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/benchmarks/task5_results.json b/EventDriven/riskmanager/position/cogs/benchmarks/task5_results.json deleted file mode 100644 index 8fa5911..0000000 --- a/EventDriven/riskmanager/position/cogs/benchmarks/task5_results.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "timestamp": "2025-11-28T21:46:57.959437", - "iterations": 200, - "num_positions": 50, - "timings_seconds": [ - 0.1089219339992269, - 0.09802097400097409, - 0.08703202399919974, - 0.11088276900045457, - 0.08726841799943941, - 0.0887409149981977, - 0.12584279300062917, - 0.1398369790003926, - 0.14138886100045056, - 0.1917432420013938, - 0.164501259001554, - 0.10886575600306969, - 0.16109909700026037, - 0.1790011160010181, - 0.27850061200297205, - 0.1507456559993443, - 0.21767190999889863, - 0.11462807700081612, - 0.13145842800076935, - 0.1914327439990302, - 0.12907303900283296, - 0.13861405300121987, - 0.14326140999764903, - 0.1269289440024295, - 0.14512716999888653, - 0.1671772770023381, - 0.12246256499929586, - 0.11078518000067561, - 0.10707120000006398, - 0.1137628640026378, - 0.10230016900095507, - 0.1825071429993841, - 0.15006474999972852, - 0.30487448799976846, - 0.10314669599756598, - 0.11122290400089696, - 0.09909520699875429, - 0.09748298100021202, - 0.17285977199935587, - 0.14450798800316988, - 0.15999174500029767, - 0.14564863700070418, - 0.1623018870013766, - 0.1505768019997049, - 0.10889471499831416, - 0.23049596000055317, - 0.11448908199963626, - 0.19225260299936053, - 0.16447250099736266, - 0.1651954509979987, - 0.19219014900227194, - 0.21509622699886677, - 0.09442325400232221, - 0.11811021199901006, - 0.10741977200086694, - 0.12378941700080759, - 0.15648619000057806, - 0.09273830199890654, - 0.06674855099845445, - 0.0757124850024411, - 0.0583316720003495, - 0.08974741400015773, - 0.08481783900060691, - 0.10336219999953755, - 0.0765874780008744, - 0.08255783100321423, - 0.08995549700193806, - 0.08111147100134986, - 0.07566160400165245, - 0.06949973799783038, - 0.08224309700017329, - 0.08162861399978283, - 0.06371668499923544, - 0.06468968599801883, - 0.09802857999966363, - 0.08314200599852484, - 0.06810568900255021, - 0.0748133710003458, - 0.07478377300139982, - 0.05910509299792466, - 0.061048618001223076, - 0.05885989599846653, - 0.07286871899850667, - 0.07594551399961347, - 0.07134067299921298, - 0.09402994600168313, - 0.08900329700190923, - 0.08878073199957726, - 0.07075972899838234, - 0.08073401000001468, - 0.07516645799842081, - 0.0707428979985707, - 0.07791603300211136, - 0.07881576799991308, - 0.07360145199709223, - 0.07437466699775541, - 0.07691888999761431, - 0.07676005300163524, - 0.08020747399859829, - 0.07258554600048228, - 0.08173184900078923, - 0.06993334100116044, - 0.07966748699982418, - 0.07864050699936342, - 0.07727664900085074, - 0.06874230100220302, - 0.1072475389992178, - 0.08666552499926183, - 0.08205463400008739, - 0.07733792900035041, - 0.0569605490018148, - 0.08128852699883282, - 0.09288220899907174, - 0.08599323100133915, - 0.11154435399657814, - 0.06861060400115093, - 0.06242495699916617, - 0.06451409000146668, - 0.07543261200044071, - 0.07094991800113348, - 0.07735480699921027, - 0.1502909289993113, - 0.08450783200169099, - 0.08682149599917466, - 0.13222767299885163, - 0.11421083200184512, - 0.058172469998680754, - 0.055735654997988604, - 0.05328968299727421, - 0.05519250400175224, - 0.05721537899808027, - 0.062530892999348, - 0.06718504600212327, - 0.06834791800065432, - 0.07306059599795844, - 0.07863593900037813, - 0.06710431299870834, - 0.06848555599935935, - 0.07531626700074412, - 0.06510260600043694, - 0.05529627900250489, - 0.059315392001735745, - 0.07427294700028142, - 0.07650540499889757, - 0.0625698400035617, - 0.0560887539977557, - 0.07187026300016441, - 0.19427029299913556, - 0.11880223299885984, - 0.10030705200188095, - 0.06772127800286398, - 0.07203302899870323, - 0.06908680900232866, - 0.06489372400028515, - 0.05595659900063765, - 0.06033889000173076, - 0.055176201996800955, - 0.060114544998214114, - 0.0531776540010469, - 0.055032790998666314, - 0.06155645700346213, - 0.053624201002094196, - 0.05433184200228425, - 0.054823727998154936, - 0.053664212002331624, - 0.05385077800019644, - 0.07174393300010706, - 0.10222304299895768, - 0.05976189200009685, - 0.05576328499955707, - 0.09459074200276518, - 0.10507479199804948, - 0.07043334799891454, - 0.08086410500254715, - 0.07043009200060624, - 0.09884305900050094, - 0.06069126900183619, - 0.060669990998576395, - 0.06453101100123604, - 0.09901672400155803, - 0.08923638200212736, - 0.07230939299915917, - 0.071288375002041, - 0.055112072001065826, - 0.05964980499993544, - 0.08920362199933152, - 0.06772691000151099, - 0.070233072998235, - 0.05972989499787218, - 0.05990275500153075, - 0.05614232500010985, - 0.05420120499911718, - 0.054162999000254786, - 0.07050136000179918, - 0.07551115000023856, - 0.06602066900086356, - 0.07187284599785926, - 0.07040314600089914, - 0.05422723800074891, - 0.056120690001989715 - ], - "avg_time_seconds": 0.09466427308017955, - "min_time_seconds": 0.0531776540010469, - "max_time_seconds": 0.30487448799976846, - "avg_time_per_position_ms": 1.893285461603591, - "total_time_seconds": 18.93285461603591 -} \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index 185d0dd..c63be76 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -244,7 +244,7 @@ ) from .vars import MEASURES_SET -logger = setup_logger("EventDriven.riskmanager.position.cogs.limits", stream_log_level="WARNING") +logger = setup_logger("EventDriven.riskmanager.position.cogs.limits", stream_log_level="INFO") @dataclass @@ -255,7 +255,7 @@ class _LimitsMetaData: scalar: float sizing_lev: float delta_lmt: float - delta: Optional[float] = None + delta_per_contract: Optional[float] = None option_price: Optional[float] = None undl_price: Optional[float] = None @@ -369,7 +369,7 @@ def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: signal_id=order["signal_id"], scalar=scalar, sizing_lev=self.sizer_configs.sizing_lev, - delta=delta, + delta_per_contract=delta, option_price=option_price, undl_price=undl_data.chain_spot["close"], delta_lmt=new_pos_state.limits.delta, @@ -433,6 +433,7 @@ def _update_position_quantity(self, new_position_state: NewPositionState) -> Non logger.warning( f"Calculated position size is 0 for order {order['data']['trade_id']}. Delta per contract ({delta}) exceeds limit {delta_lmt}." ) + logger.info(f"Updated position quantity to {q} for order {order['data']['trade_id']}.") new_position_state.order = Order.from_dict(order_dict) def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: diff --git a/EventDriven/riskmanager/sizer/_utils.py b/EventDriven/riskmanager/sizer/_utils.py index 20a4244..0a2e620 100644 --- a/EventDriven/riskmanager/sizer/_utils.py +++ b/EventDriven/riskmanager/sizer/_utils.py @@ -169,7 +169,7 @@ import pandas as pd from EventDriven.riskmanager.utils import logger from ..._vars import Y2_LAGGED_START_DATE -from ..market_data import get_timeseries_obj +from trade.datamanager.market_data import get_timeseries_obj def default_delta_limit( @@ -238,9 +238,9 @@ def delta_position_sizing( int: The calculated position size.""" ## TODO: Add docstring ## TODO: Raise error if delta is 0 or cash_available is <= 0 - if delta == 0 or cash_available <= 0 or option_price_at_time <= 0: + if delta == 0 or math.isnan(delta) or cash_available <= 0 or option_price_at_time <= 0: logger.critical( - f"Delta is 0 or cash_available is <= 0 or option_price_at_time <= 0. delta: {delta}, cash_available: {cash_available}, option_price_at_time: {option_price_at_time}. This is intended to be long only sizing. Returning 0." + f"Delta is 0/NaN or cash_available is <= 0 or option_price_at_time <= 0. delta: {delta}, cash_available: {cash_available}, option_price_at_time: {option_price_at_time}. This is intended to be long only sizing. Returning 0." ) return 0 try: @@ -368,7 +368,7 @@ def load_scalers(self, syms: list = None, force=False) -> None: """ ## Get timeseries object - timeseries = get_timeseries_obj() + timeseries = get_timeseries_obj(live=True) ## If syms is None, use the existing syms ## This is to avoid reloading timeseries if already loaded @@ -398,9 +398,9 @@ def load_scalers(self, syms: list = None, force=False) -> None: ## Load timeseries for each symbol and calculate the z-score scaler for sym in syms: timeseries.load_timeseries( - sym=sym, start_date=Y2_LAGGED_START_DATE, end_date=datetime.now(), interval=self.interval + sym=sym, start_date=Y2_LAGGED_START_DATE, end_date=datetime.now() ) - ts = timeseries.get_timeseries(sym=sym, interval=self.interval).spot["close"] + ts = timeseries.get_timeseries(sym=sym).spot["close"] if self.vol_type == "window": func = lambda x: realized_vol(x, self.rvol_window) diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index 34d9a7e..6f243e6 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -187,7 +187,9 @@ import functools from trade.assets.helpers.utils import swap_ticker from module_test.raw_code.DataManagers.DataManagers import OptionDataManager # noqa -from module_test.raw_code.DataManagers.DataManagers_cached import CachedOptionDataManager # noqa +from trade.datamanager.loaders import load_full_option_data +from trade.datamanager.vars import get_times_series +from trade.datamanager._enums import DivType from trade.helpers.helper import generate_option_tick_new, parse_option_tick, CustomCache, change_to_last_busday from dbase.DataAPI.ThetaData import retrieve_bulk_open_interest, retrieve_chain_bulk from pandas.tseries.offsets import BDay @@ -502,6 +504,7 @@ def save_to_cache(id, date, spot): ##UTILS + def load_position_data(opttick, processed_option_data, start, end, s, r, y, s0_close): """ Load position data for a given option tick. @@ -527,7 +530,7 @@ def load_position_data(opttick, processed_option_data, start, end, s, r, y, s0_c meta = parse_option_tick(opttick) ## Generate data - data = generate_spot_greeks(opttick, start_date=start, end_date=end) + data = old_generate_spot_greeks(opttick, start_date=start, end_date=end) data = enrich_data( data, meta["ticker"], @@ -540,6 +543,59 @@ def load_position_data(opttick, processed_option_data, start, end, s, r, y, s0_c return data +def load_position_data_new(opttick, processed_option_data, start, end) -> pd.DataFrame: + """ + Load position data for a given option tick using the new data loading method. + + args: + opttick (str): The option tick to load data for. + processed_option_data (dict): A dictionary to store processed option data. + start (str|datetime): The start date for the data. + end (str|datetime): The end date for the data. + + This function retrieves the data for the given option tick using the new data loading method. + It does not apply any splits or adjustments. It will only retrieve the data for the given option tick. + """ + ## Check if the option tick is already processed + if opttick in processed_option_data: + return processed_option_data[opttick] + + ## Get Meta + option_meta = parse_option_tick(opttick) + new_data = load_full_option_data( + symbol=option_meta["ticker"], + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + start_date=start, + end_date=end, + dividend_type=DivType.CONTINUOUS + ) + + ## Convert to DataFrame for easier comparison + greeks = new_data.greek.timeseries + option_spot = new_data.option_spot.timeseries + s = new_data.spot.timeseries + y = new_data.dividend.timeseries + r = new_data.rates.timeseries + s0_close = get_times_series()._get_spot_timeseries(sym=option_meta["ticker"], start=start, end=end)["close"] + s0_close.name = "s0_close" + + ## set names properly + s.name = "s" + y.name = "y" + r.name = "r" + data = greeks.join(option_spot[["midpoint", "closeask", "closebid"]]) + data.columns = data.columns.str.capitalize() + data = data.join(s).join(y).join(r).join(s0_close) + return data + + ## Generate data + data = new_generate_spot_greeks(opttick, start_date=start, end_date=end) + processed_option_data[opttick] = data + return data + + def enrich_data(data, ticker, s, r, y, s0_close): """ Args: @@ -561,7 +617,7 @@ def enrich_data(data, ticker, s, r, y, s0_close): return data -def generate_spot_greeks(opttick, start_date: str | datetime, end_date: str | datetime) -> pd.DataFrame: +def old_generate_spot_greeks(opttick, start_date: str | datetime, end_date: str | datetime) -> pd.DataFrame: """ Generate spot greeks for a given option tick. """ @@ -586,6 +642,26 @@ def generate_spot_greeks(opttick, start_date: str | datetime, end_date: str | da data = greeks.join(spot) return data +def new_generate_spot_greeks(opttick, start_date: str | datetime, end_date: str | datetime) -> pd.DataFrame: + """ + Generate spot greeks for a given option tick using the load_full_data. + """ + option_meta = parse_option_tick(opttick) + new_data = load_full_option_data( + symbol=option_meta["ticker"], + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + start_date=start_date, + end_date=end_date, + ) + ## Convert to DataFrame for easier comparison + greeks = new_data.greek.timeseries + option_spot = new_data.option_spot.timeseries + data = greeks.join(option_spot[["midpoint", "closeask", "closebid"]]) + data.columns = data.columns.str.capitalize() + return data + def parse_position_id(positionID: str) -> Tuple[dict, list]: position_str = positionID diff --git a/EventDriven/types.py b/EventDriven/types.py index b18d9c0..407cb14 100644 --- a/EventDriven/types.py +++ b/EventDriven/types.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from typing import Any, Dict, List from typing_extensions import TypedDict -from EventDriven.helpers import parse_signal_id, generate_signal_id +from EventDriven.helpers import parse_signal_id, generate_signal_id, parse_position_id class SignalID(str): @@ -34,6 +34,23 @@ def __str__(self): return self.signal_id def __repr__(self): return f"SignalID({self.signal_id})" + +class TradeID(str): + """ + Unique identifier for a trade execution. + + Format: + &L:{LONG_LEG_1}&L:{LONG_LEG_2}...&S:{SHORT_LEG_1}&S:{SHORT_LEG_2}... + """ + def __init__(self, trade_id: str): + self.trade_id = trade_id + self.meta, self.legs = parse_position_id(trade_id) + + def __str__(self): + return self.trade_id + + def __repr__(self): + return f"TradeID({self.trade_id})" class OrderDataDict(TypedDict): trade_id: str diff --git a/module_test/raw_code/DataManagers/DataManagers.py b/module_test/raw_code/DataManagers/DataManagers.py index bdebfca..0a66f41 100644 --- a/module_test/raw_code/DataManagers/DataManagers.py +++ b/module_test/raw_code/DataManagers/DataManagers.py @@ -1860,7 +1860,7 @@ def calc_vol_for_data( lambda x:IV_handler(S = x[col_kwargs['underlier_price']], K = x[col_kwargs['strike']], price = x[price_col], - t = time_distance_helper(x[col_kwargs['expiration']], x[col_kwargs['datetime']]), + t = time_distance_helper(end=x[col_kwargs['expiration']], start=x[col_kwargs['datetime']]), r = x[col_kwargs['rf_rate']], q = x[col_kwargs['dividend']], flag = x[col_kwargs['put/call']].lower()), axis = 1 @@ -1921,7 +1921,7 @@ def calc_vol_for_data_parallel( } temp_df = df.copy() temp_df.rename(columns=col_kwargs, inplace=True) - temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(x[col_kwargs['expiration']], x[col_kwargs['datetime']]), axis=1) + temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(end=x[col_kwargs['expiration']], start=x[col_kwargs['datetime']]), axis=1) binomial_column = [price_col, col_kwargs['underlier_price'], col_kwargs['strike'], col_kwargs['rf_rate'], col_kwargs['expiration'], col_kwargs['put/call'], col_kwargs['datetime'], col_kwargs['dividend'],] @@ -2049,7 +2049,7 @@ def calc_greeks_for_data_parallel( temp_df = df.copy() temp_df.rename(columns=col_kwargs, inplace=True) - temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(x[col_kwargs['expiration']], x[col_kwargs['datetime']]), axis=1) + temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(end=x[col_kwargs['expiration']], start=x[col_kwargs['datetime']]), axis=1) temp_df['asset'] = None temp_df['model'] = model greeks_colums_use = ['asset',col_kwargs['underlier_price'], col_kwargs['strike'], diff --git a/module_test/raw_code/DataManagers/DataManagers_cached.py b/module_test/raw_code/DataManagers/DataManagers_cached.py index 6ec5e53..18f9ed9 100644 --- a/module_test/raw_code/DataManagers/DataManagers_cached.py +++ b/module_test/raw_code/DataManagers/DataManagers_cached.py @@ -49,7 +49,7 @@ # Import MarketTimeseries for underlier data caching try: - from EventDriven.riskmanager.market_data import get_timeseries_obj + from trade.datamanager.market_data import get_timeseries_obj MARKET_TIMESERIES_AVAILABLE = True except ImportError: import traceback diff --git a/module_test/raw_code/DataManagers/SaveManager.py b/module_test/raw_code/DataManagers/SaveManager.py index ee0c5c4..85fc868 100644 --- a/module_test/raw_code/DataManagers/SaveManager.py +++ b/module_test/raw_code/DataManagers/SaveManager.py @@ -22,11 +22,10 @@ get_int_value, get_shared_lock, get_request_list) -from trade import is_allowed_user, USER -from trade.helpers.Logging import setup_logger +from trade import is_allowed_user from .vars import ALLOWED_SCHEDULE_USERS -logger = setup_logger('DataManager.py', stream_log_level = logging.CRITICAL) +logger = setup_logger("DataManager.py", stream_log_level=logging.CRITICAL) if is_allowed_user(ALLOWED_SCHEDULE_USERS): print('\n') print("Scheduled Data Requests will be saved to:", f"{os.environ['WORK_DIR']}/module_test/raw_code/DataManagers/scheduler/requests.jsonl") diff --git a/module_test/raw_code/DataManagers/option_cache/helpers.py b/module_test/raw_code/DataManagers/option_cache/helpers.py index 9d4e652..070b365 100644 --- a/module_test/raw_code/DataManagers/option_cache/helpers.py +++ b/module_test/raw_code/DataManagers/option_cache/helpers.py @@ -2,7 +2,6 @@ Cache helpers for DataManagers with option data caching. This module provides cache instances for option spot, volatility, and greeks data. -Follows the pattern from EventDriven.riskmanager.market_data.py for consistency. """ from __future__ import annotations diff --git a/module_test/raw_code/DataManagers/option_cache/tests_archive/test_eoddata_cache.py b/module_test/raw_code/DataManagers/option_cache/tests_archive/test_eoddata_cache.py index 1a7d64b..d295f5a 100644 --- a/module_test/raw_code/DataManagers/option_cache/tests_archive/test_eoddata_cache.py +++ b/module_test/raw_code/DataManagers/option_cache/tests_archive/test_eoddata_cache.py @@ -43,7 +43,7 @@ # Try to import and create MarketTimeseries try: - from EventDriven.riskmanager.market_data import get_timeseries_obj + from trade.datamanager.market_data import get_timeseries_obj market_ts = get_timeseries_obj() set_global_market_timeseries(market_ts) print(f" ✓ Created and set MarketTimeseries: {market_ts}\n") diff --git a/module_test/raw_code/DataManagers/utils.py b/module_test/raw_code/DataManagers/utils.py index c710a76..83762ef 100644 --- a/module_test/raw_code/DataManagers/utils.py +++ b/module_test/raw_code/DataManagers/utils.py @@ -17,8 +17,6 @@ def set_global_market_timeseries(market_timeseries_instance): """ Set the global MarketTimeseries instance for caching underlier data. - Args: - market_timeseries_instance: MarketTimeseries instance from EventDriven.riskmanager.market_data """ global _GLOBAL_MARKET_TIMESERIES _GLOBAL_MARKET_TIMESERIES = market_timeseries_instance diff --git a/module_test/raw_code/optionlib/core/time_utils.py b/module_test/raw_code/optionlib/core/time_utils.py index 112d4d9..ac0bafe 100644 --- a/module_test/raw_code/optionlib/core/time_utils.py +++ b/module_test/raw_code/optionlib/core/time_utils.py @@ -1,26 +1,27 @@ from datetime import datetime, timedelta import pandas as pd +from trade.helpers.helper import time_distance_helper # noqa # import numpy as np -def time_distance_helper(end: str, strt: str = None) -> float: - """ - Calculate the time distance between two dates in years. - Args: - end (str): Expiration date/End Date in 'YYYY-MM-DD' format. - strt (str, optional): Start date in 'YYYY-MM-DD' format. Defaults to today's date. - Returns: - float: Time distance in years. - """ - if strt is None: - strt = datetime.today() +# def time_distance_helper(end: str, strt: str = None) -> float: +# """ +# Calculate the time distance between two dates in years. +# Args: +# end (str): Expiration date/End Date in 'YYYY-MM-DD' format. +# strt (str, optional): Start date in 'YYYY-MM-DD' format. Defaults to today's date. +# Returns: +# float: Time distance in years. +# """ +# if strt is None: +# strt = datetime.today() - end = pd.to_datetime(end) - end = end.replace(hour = 16, minute = 0, second = 0, microsecond = 0,) - parsed_dte, start_date = pd.to_datetime(end), pd.to_datetime(strt) - if start_date.hour == 0 and start_date.minute == 0 and start_date.second == 0: - start_date = start_date.replace(hour=16, minute=0, second=0, microsecond=0) - days = (parsed_dte - start_date).total_seconds() +# end = pd.to_datetime(end) +# end = end.replace(hour = 16, minute = 0, second = 0, microsecond = 0,) +# parsed_dte, start_date = pd.to_datetime(end), pd.to_datetime(strt) +# if start_date.hour == 0 and start_date.minute == 0 and start_date.second == 0: +# start_date = start_date.replace(hour=16, minute=0, second=0, microsecond=0) +# days = (parsed_dte - start_date).total_seconds() - T = days/(365.25*24*3600) - return T \ No newline at end of file +# T = days/(365.25*24*3600) +# return T \ No newline at end of file diff --git a/pricingConfig.json b/pricingConfig.json index 81d3633..677a076 100644 --- a/pricingConfig.json +++ b/pricingConfig.json @@ -1,6 +1,6 @@ { "INTRADAY_AGG": "30m", - "MARKET_OPEN_TIME": "09:30", + "MARKET_OPEN_TIME": "09:00", "MARKET_CLOSE_TIME": "16:00", "AVAILABLE_PRICING_MODELS": [ "bs", @@ -30,7 +30,7 @@ "DAYS_IN_MONTH": 30, "DAYS_IN_YEAR": 360, "MIN_BAR_TIME_INTERVAL": "5m", - "QUOTE_DATA_START_TIME": "9:45:00", + "QUOTE_DATA_START_TIME": "09:15:00", "VOL_SURFACE_MIN_MONEYNESS_THRESHOLD": 0.1, "VOL_SURFACE_MAX_MONEYNESS_THRESHOLD": 2.0, "VOL_SURFACE_MIN_DTE_THRESHOLD": 30, diff --git a/ruff.toml b/ruff.toml index 8bf45f8..307ac75 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,3 +1,5 @@ line-length = 120 + +[lint] extend-select = ["B", "I"] ignore = ["E501", "I001", "E731", "B009", "E722"] \ No newline at end of file diff --git a/running_todo.todo b/running_todo.todo new file mode 100644 index 0000000..125a119 --- /dev/null +++ b/running_todo.todo @@ -0,0 +1,37 @@ +Running Todo: + + ☐ Write steps to add new strategy to backtest framework + ☐ Write steps to add new strategy to live trading framework + ☐ Check if thetadata v3 is backward compatible with v2 + ☐ Write proper docstring for binomial model in optlib + + ☐ How do we handle reverse splits? + ☐ Write a ChainDataManager + ☐ Add full dataframe tp Spot Result + - Include price property to pick from columns. Create enum for standard columns + ☐ Remove all import for riskmanager.market_data + ☐ Save scaling in position sizing info to database + ☐ Make order picker faster & more efficient + ☐ Singleton Metaclass with attribute picking + ☐ Find a way to save historically closed in signals tabled. + ☐ Add slippage pnl to live attribution + ☐ Aggregator should take trades & equity data only + ☐ EVB should use StrategyBase class (Optionally, dataframe, but probably should discourage it) + ☐ Update start date in Backtesting.py stats generated during PTBacktester. + ☐ Discontinue all use of module_test.raw_code.DataManagers + ☐ Add refresh information to MarketTimeseries to avoid constantly refreshing data when not needed. + ☐ Move all vol and pricing logic to use the new OptLib + ☐ Eg FillOptimizer + ☐ Ensure MarketTimeseries is not cutting out dividends or split factors for real time + ☐ Ensure make position id & signal id comes from one place + ☐ Update get_risk_free_rate_helper to use RatesDataManager + ☐ Look into alternatives for backfilling dividends with 0.0 in get_timeseries + +Todo before going back to backtesting: + ☐ For test order gen, it should use PREVIOU day's cash of order gen, not current cash. + ✔ Allow preset orders in Evb order getter @done(26-02-09 21:14) + ☐ Clean up evb order getter + preset orders + ✔ Close signals added to signals table @done(26-02-09 16:34) + ☐ Ensure backtesting a live strategy works seamlessly + ☐ Decipher the diff btwn backtest and live strategy performance + ☐ I don't like how limit sizing is going. Look into it. \ No newline at end of file diff --git a/todo.todo b/todo.todo index be67901..3bdf743 100644 --- a/todo.todo +++ b/todo.todo @@ -1,3 +1,5 @@ +## This is repo specific todo. + Todo Todo: ☐ Review this file and cut down useless tasks diff --git a/trade/.DS_Store b/trade/.DS_Store index c831bb3..51e64a7 100644 Binary files a/trade/.DS_Store and b/trade/.DS_Store differ diff --git a/trade/__init__.py b/trade/__init__.py index 0aa7537..12e2470 100644 --- a/trade/__init__.py +++ b/trade/__init__.py @@ -10,17 +10,23 @@ from dotenv import load_dotenv from trade.helpers.clear_cache import cleanup_expired_caches from .helpers.Logging import setup_logger +from pathlib import Path warnings.filterwarnings("ignore") +# Load .env file first before accessing any environment variables +load_dotenv() -USER = str(os.environ.get("USER", "unknown_user")).lower() ## Temporary fix to allow only chidi utilize some features +USER = str(os.environ.get("USER", "unknown_user")).lower() ## Temporary fix to allow only chidi utilize some features +GEN_CACHE_PATH = Path(os.environ.get("GEN_CACHE_PATH", Path(os.environ.get("WORK_DIR", ".")) / ".cache")) +TIMING_ANALYSIS_CACHE_PATH = GEN_CACHE_PATH / "timing_analysis" +TIMING_ANALYSIS_CACHE_PATH.mkdir(parents=True, exist_ok=True) POOL_ENABLED = None SIGNALS_TO_RUN = {} EXIT_HANDLERS = [] # Handlers for normal program exit _ATEXIT_REGISTERED = False OWNER_PID = os.getpid() -logger = setup_logger('trade.__init__') +logger = setup_logger("trade.__init__", stream_log_level="WARNING") cleanup_expired_caches() @@ -28,18 +34,36 @@ ## Get Business days FOR NYSE (Some days are still trading days) ##TODO: Make this more dynamic, so it can be used for other exchanges as well. And end date should be dynamic as well. NY = ZoneInfo("America/New_York") -nyse = mcal.get_calendar('NYSE') -schedule = nyse.schedule(start_date='2000-01-01', end_date='2040-01-01', tz=NY) +nyse = mcal.get_calendar("NYSE") +schedule = nyse.schedule(start_date="2000-01-01", end_date="2040-01-01", tz=NY) # pylint: disable=E1101 -all_trading_days = mcal.date_range(schedule, frequency='1D').date ## type: ignore -all_days = pd.date_range(start='2000-01-01', end='2040-01-01', freq='B') -holidays = set(all_days.difference(all_trading_days).strftime('%Y-%m-%d').to_list()) +all_trading_days = mcal.date_range(schedule, frequency="1D").date ## type: ignore +all_days = pd.date_range(start="2000-01-01", end="2040-01-01", freq="B") +holidays = set(all_days.difference(all_trading_days).strftime("%Y-%m-%d").to_list()) HOLIDAY_SET = set(holidays) +DATETIME_HOLIDAY_SET = set(pd.to_datetime(list(HOLIDAY_SET), format="%Y-%m-%d")) ## Additional holidays -HOLIDAY_SET.update({ - '2025-01-09', ## Jimmy Carter's Death -}) +HOLIDAY_SET.update( + { + "2025-01-09", ## Jimmy Carter's Death + } +) + + +def get_current_user() -> str: + """ + Get the current user's name from the USER environment variable. + + Returns: + ------- + str + The current user's name (lowercase). + """ + user = str(os.environ.get("QUANTTOOLS_USER", "unknown_user")).lower() + if user == "unknown_user": + logger.warning("USER environment variable is not set. Please set it for proper user identification.") + return user def is_allowed_user(allowed_users: list) -> bool: @@ -57,12 +81,10 @@ def is_allowed_user(allowed_users: list) -> bool: True if the current user is allowed, False otherwise. """ allowed_users = [user.lower() for user in allowed_users] - if USER.lower() in allowed_users: + if get_current_user() in allowed_users: return True else: - logger.warning("User %s is not allowed to perform this action.", USER) return False - def _run_exit_handlers(): @@ -86,7 +108,7 @@ def register_signal(signum, signal_func): The signal number (e.g., signal.SIGINT, signal.SIGTERM) or 'exit' for normal program exit. signal_func : callable The function to execute when the signal is received or program exits. - + Examples: -------- >>> register_signal(signal.SIGTERM, cleanup_function) @@ -94,12 +116,12 @@ def register_signal(signum, signal_func): >>> register_signal('exit', save_data_function) # For normal program exit """ global _ATEXIT_REGISTERED - + if not callable(signal_func): raise ValueError(f"Signal function {signal_func} is not callable.") - + # Handle normal program exit - if signum == 'exit' or signum == 0: + if signum == "exit" or signum == 0: EXIT_HANDLERS.append(signal_func) # Register atexit handler only once if not _ATEXIT_REGISTERED: @@ -108,13 +130,13 @@ def register_signal(signum, signal_func): logger.info("Registered atexit handler for normal program exit.") logger.info("Exit handler `%s` registered for normal program exit.", signal_func.__name__) return - + # Handle signal-based interrupts if signum not in SIGNALS_TO_RUN: SIGNALS_TO_RUN[signum] = [] signal.signal(signum, run_signals) logger.info("Registered signal number %d.", signum) - + SIGNALS_TO_RUN[signum].append(signal_func) logger.info("Signal function for `%s` added to signal number %d.", signal_func.__name__, signum) @@ -123,11 +145,20 @@ def run_signals(signum, frame): """ Run all registered signals. """ + ALREADY_RAN = [] if os.getpid() != OWNER_PID: logger.info("Signal received in child process (PID: %d). Ignoring signal %d.", os.getpid(), signum) return + + logger.info("Signal %d received - running ALL cleanup handlers", signum) + if signum in SIGNALS_TO_RUN: for signal_func in SIGNALS_TO_RUN[signum]: + if signal_func in ALREADY_RAN: + logger.info("Signal function %s for signal %d has already run. Skipping.", signal_func.__name__, signum) + continue + + ALREADY_RAN.append(signal_func) try: logger.info("Running signal function %s for signal %d.", signal_func.__name__, signum) signal_func() @@ -135,7 +166,10 @@ def run_signals(signum, frame): logger.info("Error running signal function %s: %s", signal_func.__name__, e) else: logger.info("No registered signals for signal number %d.", signum) - + + # Run exit handlers + _run_exit_handlers() + # Actually terminate the program after cleanup for interrupt/termination signals if signum in (signal.SIGINT, signal.SIGTERM): logger.info("Exiting after signal %d.", signum) @@ -152,13 +186,14 @@ def str_to_bool(value: str) -> bool: Returns: bool: True if the string is 'True', '1', or 'yes' (case-insensitive), False otherwise. """ - if value.lower() in ['true', '1', 'yes']: + if value.lower() in ["true", "1", "yes"]: return True - elif value.lower() in ['false', '0', 'no']: + elif value.lower() in ["false", "0", "no"]: return False else: raise ValueError("Invalid boolean string. Expected 'True', 'False', '1', '0', 'yes', or 'no'.") - + + def get_signals_to_run(): """ Get the registered signals to run. @@ -174,25 +209,28 @@ def set_pool_enabled(value: bool): global POOL_ENABLED POOL_ENABLED = value + def get_pool_enabled(): """ Get the pool enabled flag. """ return POOL_ENABLED + def reset_pool_enabled(): """ Reset the pool enabled flag to None. """ - load_dotenv(f"{os.environ['WORK_DIR']}/.env") - set_pool_enabled(str_to_bool(os.environ.get('POOL_ENABLED', 'False'))) + # .env already loaded at module import, just reload if needed + load_dotenv(override=False) + set_pool_enabled(str_to_bool(os.environ.get("POOL_ENABLED", "False"))) -reset_pool_enabled() +reset_pool_enabled() ## Import Pricing Config -with open(f"{os.environ['WORK_DIR']}/pricingConfig.json", encoding='utf-8') as f: +with open(f"{os.environ['WORK_DIR']}/pricingConfig.json", encoding="utf-8") as f: PRICING_CONFIG = json.load(f) @@ -201,12 +239,12 @@ def get_pricing_config() -> dict: Get the pricing configuration. """ MISSING_DEFAULTS = { - 'VOL_SURFACE_MAX_DTE_THRESHOLD': 365, - 'VOL_SURFACE_MIN_DTE_THRESHOLD': 0, - 'VOL_SURFACE_MAX_MONEYNESS_THRESHOLD': 1, - 'VOL_SURFACE_MIN_MONEYNESS_THRESHOLD': 0 + "VOL_SURFACE_MAX_DTE_THRESHOLD": 365, + "VOL_SURFACE_MIN_DTE_THRESHOLD": 0, + "VOL_SURFACE_MAX_MONEYNESS_THRESHOLD": 1, + "VOL_SURFACE_MIN_MONEYNESS_THRESHOLD": 0, } - with open(f"{os.environ['WORK_DIR']}/pricingConfig.json", encoding='utf-8') as f: + with open(f"{os.environ['WORK_DIR']}/pricingConfig.json", encoding="utf-8") as f: PRICING_CONFIG = json.load(f) for key, value in MISSING_DEFAULTS.items(): @@ -215,18 +253,14 @@ def get_pricing_config() -> dict: logger.warning(f"Missing key {key} in pricing config. Setting default value {value}.") return PRICING_CONFIG + def reload_pricing_config(): """ Reload the pricing configuration from the file. """ - - global PRICING_CONFIG - with open(f"{os.environ['WORK_DIR']}/pricingConfig.json", encoding='utf-8') as pricing_file: + with open(f"{os.environ['WORK_DIR']}/pricingConfig.json", encoding="utf-8") as pricing_file: PRICING_CONFIG = json.load(pricing_file) - logger.info("Pricing configuration reloaded.") - - diff --git a/trade/assets/Calculate.py b/trade/assets/Calculate.py index 5074e61..5fad186 100644 --- a/trade/assets/Calculate.py +++ b/trade/assets/Calculate.py @@ -608,7 +608,7 @@ def delta(asset = None, S = None, K = None, r = None, sigma = None, start = None if args[i] is None: args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) flag = getattr(asset, OptionModelAttributes.put_call.value) if model == 'bs': d = delta(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) @@ -625,7 +625,7 @@ def delta(asset = None, S = None, K = None, r = None, sigma = None, start = None if sigma == 0: raise ValueError("Sigma cannot be 0") - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': d = delta(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) elif model == 'binomial': @@ -660,7 +660,7 @@ def vega(asset = None, S = None, K = None, r = None, sigma = None, start = None, args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) flag = getattr(asset, OptionModelAttributes.put_call.value) if model == 'bs': d = vega(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) @@ -670,7 +670,7 @@ def vega(asset = None, S = None, K = None, r = None, sigma = None, start = None, elif asset == None: assert all(v is not None for v in [S, K, r, sigma, start, flag, exp, y]), f"None of y, S, K, r, sigma, start, flag, exp, can be None" - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': d = vega(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) elif model == 'binomial': @@ -706,7 +706,7 @@ def vanna(asset = None, S = None, K = None, r = None, sigma = None, start = None if args[i] is None: args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) flag = getattr(asset, OptionModelAttributes.put_call.value) if model == 'bs': # d = vanna(flag = flag.lower(),S = args[0], K = args[1], T = t, r = args[2], sigma = args[3], q = args[4] ) @@ -725,7 +725,7 @@ def vanna(asset = None, S = None, K = None, r = None, sigma = None, start = None elif asset == None: assert all(v is not None for v in [S, K, r, sigma, start, flag, exp, y]), f"None of y, S, K, r, sigma, start, flag, exp, can be None" - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': # d = vanna(flag = flag.lower(), S = S, K = K, T = t, r = r, sigma = sigma, q = y ) @@ -762,7 +762,7 @@ def volga(asset = None, S = None, K = None, r = None, sigma = None, start = None args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) flag = getattr(asset, OptionModelAttributes.put_call.value) if model == 'bs': d = volga_from_vega(s= args[0], k = args[1], t = t, r = args[2], sigma = args[3], q = args[4], flag= flag.lower()) @@ -772,7 +772,7 @@ def volga(asset = None, S = None, K = None, r = None, sigma = None, start = None elif asset == None: assert all(v is not None for v in [S, K, r, sigma, start, flag, exp, y]), f"None of y, S, K, r, sigma, start, flag, exp, can be None" - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': # d = volga(flag = flag.lower(), S = S, K = K, T = t, r = r, sigma = sigma, q = y ) d = volga_from_vega(s= S, k = K, t = t, r = r, sigma = sigma, q = y, flag= flag.lower()) @@ -807,7 +807,7 @@ def gamma(asset = None, S = None, K = None, r = None, sigma = None, start = None if args[i] is None: args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) if model == 'bs': d = gamma(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) elif model == 'binomial': @@ -821,7 +821,7 @@ def gamma(asset = None, S = None, K = None, r = None, sigma = None, start = None logger.error(f"Kwargs: {locals()}") return 0.0 - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': d = gamma(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) elif model == 'binomial': @@ -859,7 +859,7 @@ def theta(asset = None, S = None, K = None, r = None, sigma = None, start = None for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) if model == 'bs': d = theta(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) elif model == 'binomial': @@ -868,7 +868,7 @@ def theta(asset = None, S = None, K = None, r = None, sigma = None, start = None elif asset == None: assert all(v is not None for v in [S, K, r, sigma, start, flag, exp, y]), f"None of y, S, K, r, sigma, start, flag, exp, can be None" - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': d = theta(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) elif model == 'binomial': @@ -902,7 +902,7 @@ def rho(asset = None, S = None, K = None, r = None, sigma = None, start = None, for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - t = time_distance_helper(asset.exp, asset.end_date) + t = time_distance_helper(end=asset.exp, start=asset.end_date) if model == 'bs': d = rho(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) elif model == 'binomial': @@ -911,7 +911,7 @@ def rho(asset = None, S = None, K = None, r = None, sigma = None, start = None, elif asset == None: assert all(v is not None for v in [S, K, r, sigma, start, flag, exp, y]), f"None of y, S, K, r, sigma, start, flag, exp, can be None" - t = time_distance_helper(exp, start) + t = time_distance_helper(end=exp, start=start) if model == 'bs': d = rho(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) elif model == 'binomial': diff --git a/trade/assets/OptionChain.py b/trade/assets/OptionChain.py index 0fd4b20..5ba9773 100644 --- a/trade/assets/OptionChain.py +++ b/trade/assets/OptionChain.py @@ -59,7 +59,7 @@ def get_set( ) -> dict: try: price = retrieve_quote(ticker, date, exp, right, date, strike, start_time = '9:00')['Midpoint'][-1] #To-do: Handle None values - vol = IV_handler(S = spot, K = strike, t = time_distance_helper(exp = exp, strt = date), r = r, flag = right.lower(), price = price, q = q) + vol = IV_handler(S = spot, K = strike, t = time_distance_helper(end = exp, start = date), r = r, flag = right.lower(), price = price, q = q) except Exception as e: logger.error(f'Error in get_set: {e}', exc_info=True) raise e diff --git a/trade/assets/Stock.py b/trade/assets/Stock.py index ca24818..842fa67 100644 --- a/trade/assets/Stock.py +++ b/trade/assets/Stock.py @@ -364,9 +364,9 @@ def init_risk_free_rate(self): self.__rf_rate = ts.loc[last_available_date, "annualized"] else: - self.__rf_rate = ts[ts.index == pd.to_datetime(last_bus).strftime("%Y-%m-%d")]["annualized"].values[0] + self.__rf_rate = ts[ts.index.date == pd.to_datetime(last_bus).date()]["annualized"].values[0] else: - self.__rf_rate = ts[ts.index == pd.to_datetime(last_bus).strftime("%Y-%m-%d")]["annualized"].values[0] + self.__rf_rate = ts[ts.index.date == pd.to_datetime(last_bus).date()]["annualized"].values[0] def rebuild_chain(self): """ diff --git a/trade/assets/attribution.py b/trade/assets/attribution.py index ceb09bf..0ad7605 100644 --- a/trade/assets/attribution.py +++ b/trade/assets/attribution.py @@ -51,7 +51,7 @@ def pnl_data_organizer_helper(strike, exp, flag, op_ts = None, stock_ts = None, vol = implied_vol_bt(S0= S0, K = K,r=r, market_price=market_price,exp_date = exp,flag = flag,start=start) if math.isnan(vol): try: - vol = implied_volatility(price = market_price, S = S0, K = K, t = time_distance_helper(exp, start), r = r, q = 0, flag = flag) + vol = implied_volatility(price = market_price, S = S0, K = K, t = time_distance_helper(end=exp, start=start), r = r, q = 0, flag = flag) except: vol = np.nan merged.at[index, 'vol'] = vol diff --git a/trade/assets/helpers/DataManagers.py b/trade/assets/helpers/DataManagers.py index fe46e14..fce262f 100644 --- a/trade/assets/helpers/DataManagers.py +++ b/trade/assets/helpers/DataManagers.py @@ -342,7 +342,7 @@ def get_vol(self, type_ = 'close', query_date = datetime.now()): price = x[p], S = s0, K = self.strike, - t = time_distance_helper(exp = self.exp, strt = query_date), + t = time_distance_helper(end = self.exp, start = query_date), r = r, q = y, flag = self.right.lower()), axis = 1) @@ -617,7 +617,7 @@ def __bs_vol(self, data, price) -> pd.Series: price = x[price], S = x['underlier_price'], K = x['strike'], - t = time_distance_helper(exp = x['expiration'], strt = x['datetime']), + t = time_distance_helper(end = x['expiration'], start = x['datetime']), r = x['rf_rate'], q = x['dividend'], flag = x['put/call'].lower()), axis = 1) diff --git a/trade/assets/helpers/DataManagers_new/DataManagers.py b/trade/assets/helpers/DataManagers_new/DataManagers.py index a332ba7..998aa89 100644 --- a/trade/assets/helpers/DataManagers_new/DataManagers.py +++ b/trade/assets/helpers/DataManagers_new/DataManagers.py @@ -1548,7 +1548,7 @@ def calc_vol_for_data( lambda x:IV_handler(S = x[col_kwargs['underlier_price']], K = x[col_kwargs['strike']], price = x[price_col], - t = time_distance_helper(x[col_kwargs['expiration']], x[col_kwargs['datetime']]), + t = time_distance_helper(end = x[col_kwargs['expiration']], start = x[col_kwargs['datetime']]), r = x[col_kwargs['rf_rate']], q = x[col_kwargs['dividend']], flag = x[col_kwargs['put/call']].lower()), axis = 1 @@ -1606,7 +1606,7 @@ def calc_vol_for_data_parallel( } temp_df = df.copy() temp_df.rename(columns=col_kwargs, inplace=True) - temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(x[col_kwargs['expiration']], x[col_kwargs['datetime']]), axis=1) + temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(end = x[col_kwargs['expiration']], start = x[col_kwargs['datetime']]), axis=1) binomial_column = [price_col, col_kwargs['underlier_price'], col_kwargs['strike'], col_kwargs['rf_rate'], col_kwargs['expiration'], col_kwargs['put/call'], col_kwargs['datetime'], col_kwargs['dividend'],] @@ -1729,7 +1729,7 @@ def calc_greeks_for_data_parallel( temp_df = df.copy() temp_df.rename(columns=col_kwargs, inplace=True) - temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(x[col_kwargs['expiration']], x[col_kwargs['datetime']]), axis=1) + temp_df['t'] = temp_df.apply(lambda x: time_distance_helper(end = x[col_kwargs['expiration']], start = x[col_kwargs['datetime']]), axis=1) temp_df['asset'] = None temp_df['model'] = model greeks_colums_use = ['asset',col_kwargs['underlier_price'], col_kwargs['strike'], diff --git a/trade/assets/rates.py b/trade/assets/rates.py index 4eec786..7d75e17 100644 --- a/trade/assets/rates.py +++ b/trade/assets/rates.py @@ -10,10 +10,11 @@ import yfinance as yf import pandas as pd import warnings -from trade.helpers.helper import change_to_last_busday, setup_logger, retrieve_timeseries +from trade.helpers.helper import change_to_last_busday, setup_logger, ny_now from threading import Thread import logging from pandas.tseries.offsets import BDay +import time warnings.filterwarnings("ignore") logger = setup_logger("rates") @@ -25,6 +26,7 @@ ## Rates cache variable _rates_cache = None +DAILY_RATES_CACHE = None def reset_rates_cache(): @@ -32,7 +34,9 @@ def reset_rates_cache(): Reset the rates cache """ global _rates_cache + global DAILY_RATES_CACHE _rates_cache = None + DAILY_RATES_CACHE = None logger.info("Rates cache reset") @@ -40,7 +44,7 @@ def deannualize(annual_rate, periods=365): return (1 + annual_rate) ** (1 / periods) - 1 -def get_risk_free_rate_helper(interval="1d", use="db"): +def get_risk_free_rate_helper(interval="1d", use="db") -> pd.DataFrame: # download 3-month us treasury bills rates """ Return timeseries of 3-month US treasury bills rates @@ -54,9 +58,10 @@ def get_risk_free_rate_helper(interval="1d", use="db"): Source of the data. Default is 'yf', other option is 'db' """ - data = _fetch_rates(interval=interval).copy() - data = resample(data, interval) + + if interval != "1d": + data = resample(data, interval) data = data[~data.index.duplicated(keep="first")] return data ## Not adding the resample schema for now @@ -66,8 +71,29 @@ def _fetch_rates(interval): Handles _rates_cache logic picking """ global _rates_cache + global DAILY_RATES_CACHE + + choice_cache = _rates_cache if interval != "1d" else DAILY_RATES_CACHE + + if ny_now().hour < 25: + print("Fetching rates data from yfinance directly during market hours") + ## Just use yfinance directly during market hours to avoid stale data + data = yf.download( + "^IRX", + start="2010-01-01", + end=(datetime.datetime.today() + BDay(1)).strftime("%Y-%m-%d"), + interval="1d", + progress=False, + multi_level_index=False, + ) + + data["daily"] = data["Close"].apply(deannualize) + data["annualized"] = data["Close"] / 100 + return data[["daily", "annualized"]] + + resample_bool = interval != "1d" or DAILY_RATES_CACHE is None ## First check data base. - if _rates_cache is None: + if choice_cache is None: data = query_database( "securities_master", "rates_timeseries", @@ -78,10 +104,13 @@ def _fetch_rates(interval): data.rename(columns={"daily_rate": "daily", "annualized_rate": "annualized", "yf_tick": "name"}, inplace=True) data.index.name = "Datetime" else: - data = _rates_cache.copy() + data = choice_cache.copy() ## Drop today's date to ensure forced update - data = data[data.index.date < change_to_last_busday(datetime.datetime.now()).date()] + if ny_now().hour >= 16: + data = data[data.index.date <= change_to_last_busday(datetime.datetime.now()).date()] + else: + data = data[data.index.date < change_to_last_busday(datetime.datetime.now()).date()] ## Now, if data is not up to date, update it if data.index.max().date() < change_to_last_busday(datetime.datetime.now()).date(): @@ -110,9 +139,16 @@ def _fetch_rates(interval): data = pd.concat([data, data_min]) data = data[~data.index.duplicated(keep="first")] - _rates_cache = resample( - data, "30m", {"daily": "ffill", "annualized": "ffill", "name": "ffill", "description": "ffill"} - ) + ## Have to resample all intervals + resample_int = interval if interval == "1d" else "30m" + if resample_bool: + data = resample( + data, resample_int, {"daily": "ffill", "annualized": "ffill", "name": "ffill", "description": "ffill"} + ) + if interval != "1d": + _rates_cache = data.copy() + else: + DAILY_RATES_CACHE = data.copy() return data diff --git a/trade/backtester_/_sample.py b/trade/backtester_/_sample.py index 3c76904..6cc88bc 100644 --- a/trade/backtester_/_sample.py +++ b/trade/backtester_/_sample.py @@ -1,3 +1,5 @@ +##TODO: DELETE FILE IF UNUSED## + """ Strategy Base Classes for Backtesting Framework diff --git a/trade/backtester_/_strategy.py b/trade/backtester_/_strategy.py index 51ac30e..c41244d 100644 --- a/trade/backtester_/_strategy.py +++ b/trade/backtester_/_strategy.py @@ -8,6 +8,8 @@ import numpy as np from plotly.subplots import make_subplots import plotly.graph_objects as go +from pandas.tseries.offsets import BDay # noqa +from trade.helpers.helper import change_to_last_busday # noqa from ._types import Side, SideInt # noqa @@ -167,7 +169,12 @@ def __init_subclass__(cls, **kwargs): ) def __init__( - self, data: PTDataset, start_trading_date: Optional[str] = None, ticker: Optional[str] = None, **kwargs + self, + data: PTDataset, + start_trading_date: Optional[str] = None, + ticker: Optional[str] = None, + tplusn: Optional[int | float] = 1, + **kwargs ): """ Initializes the strategy with data and parameters. @@ -175,6 +182,8 @@ def __init__( - data: PTDataset containing the market data. - start_trading_date: Optional start date for trading (YYYY-MM-DD). - kwargs: Additional parameters defined in bt_params. + - ticker: Optional ticker symbol for the strategy. + - tplusn: Optional time offset parameter. Please always call super().__init__() in subclass __init__. """ @@ -182,6 +191,7 @@ def __init__( self.data: PTDataset = data self.start_date = pd.Timestamp(start_trading_date) if start_trading_date else None self.ticker = ticker + self.tplusn = tplusn self.position_open: bool = False self.position_side: Optional[SideInt] = SideInt.BUY @@ -698,6 +708,7 @@ def simulate(self, finalize: bool = True) -> Tuple[List[Dict[str, Any]], pd.Seri n = self._n close = self._close dates = self._index # pd.DatetimeIndex for consistent timestamps + tn = self.tplusn # noqa trades = [] equity = np.empty(n, dtype=float) @@ -893,6 +904,7 @@ def plot_strategy_indicators(self, log_scale: bool = True, add_signal_marker: bo height=300 * (1 + num_non_overlay), title_text=f"Strategy Indicators for {self.__class__.__name__}", showlegend=True, + width=1000, ) fig.update_layout(xaxis=dict(rangeslider=dict(visible=False))) fig.update_xaxes(rangebreaks=[dict(bounds=["sat", "mon"])]) # hide weekends @@ -942,7 +954,7 @@ def plot_signals(self, log_scale: bool = True) -> go.Figure: name = self.__class__.__name__ if self.ticker is None else f"{self.ticker} - {self.__class__.__name__}" fig.update_layout( height=550, - width=1500, + width=1000, title_text=f"Strategy Signals for {name}", showlegend=True, ) diff --git a/trade/backtester_/_strategy_patch.py b/trade/backtester_/_strategy_patch.py index 0ef5568..286da61 100644 --- a/trade/backtester_/_strategy_patch.py +++ b/trade/backtester_/_strategy_patch.py @@ -1,4 +1,4 @@ - +## TODO: DELETE FILE IF UNUSED## import pandas as pd from datetime import datetime, date diff --git a/trade/datamanager/README.md b/trade/datamanager/README.md new file mode 100644 index 0000000..64e8943 --- /dev/null +++ b/trade/datamanager/README.md @@ -0,0 +1,845 @@ +# QuantTools DataManager Module + +Comprehensive data infrastructure for quantitative options trading and backtesting. + +## Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Architecture](#architecture) +- [Core Data Managers](#core-data-managers) +- [Derived Metrics Managers](#derived-metrics-managers) +- [Unified Timeseries Interface](#unified-timeseries-interface) +- [Convenience Loaders](#convenience-loaders) +- [Configuration](#configuration) +- [Caching System](#caching-system) +- [Best Practices](#best-practices) + +--- + +## Overview + +The DataManager module provides a complete data infrastructure for options trading with: + +- **Historical and real-time market data** from multiple sources (ThetaData, OpenBB, YFinance) +- **Intelligent multi-tier caching** (memory + disk with expiration) +- **Derived metrics calculation** (forwards, volatilities, greeks, theoretical prices) +- **Type-safe result containers** with full metadata +- **Singleton pattern** per symbol for efficient resource management +- **Consistent API** across all managers + +### Design Principles + +- Automatic data loading from multiple sources +- Split adjustment handling for accurate backtesting +- Dividend schedule construction (discrete/continuous) +- Forward price computation with carry models +- Implied volatility calculation (BSM, Binomial) +- Greek calculation with multiple pricing models +- Theoretical pricing and scenario analysis + +--- + +## Quick Start + +### Basic Usage + +```python +from trade.datamanager import SpotDataManager, VolDataManager, GreekDataManager +from trade.datamanager._enums import DivType, OptionPricingModel + +# Load spot prices +spot_mgr = SpotDataManager("AAPL") +spot_result = spot_mgr.get_spot_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + undo_adjust=True # Split-adjusted prices +) +prices = spot_result.daily_spot + +# Get implied volatilities +vol_mgr = VolDataManager("AAPL") +vol_result = vol_mgr.get_implied_volatility_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call", + dividend_type=DivType.DISCRETE +) +ivs = vol_result.timeseries + +# Compute option greeks +greek_mgr = GreekDataManager("AAPL") +greek_result = greek_mgr.get_greeks_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call" +) +greeks_df = greek_result.timeseries # DataFrame with delta, gamma, vega, etc. +``` + +### Using the Unified Interface + +```python +from trade.datamanager.timeseries import TimeseriesDataManager + +# Single entry point for all data types +ts = TimeseriesDataManager("AAPL") + +# Consistent interface across all managers +spot = ts.spot.get_timeseries(start_date="2025-01-01", end_date="2025-01-31") +vol = ts.vol.get_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call" +) +greeks = ts.greeks.rt(strike=150.0, expiration="2025-06-20", right="call") +``` + +### One-Call Data Loading + +```python +from trade.datamanager.loaders import load_full_option_data +from trade.datamanager._enums import DivType + +# Load all option data (spot, forward, dividend, vol, greeks, rates) in one call +pack = load_full_option_data( + symbol="AAPL", + strike=150.0, + expiration="2025-06-20", + right="call", + start_date="2025-01-01", + end_date="2025-01-31", + dividend_type=DivType.DISCRETE +) + +# Access individual components +spot = pack.spot.timeseries +vol = pack.vol.timeseries +greeks = pack.greek.timeseries +``` + +--- + +## Architecture + +### Component Hierarchy + +``` +BaseDataManager (ABC) +│ +├── Market Data Layer +│ ├── SpotDataManager - Underlying equity prices +│ ├── RatesDataManager - Risk-free interest rates +│ ├── DividendDataManager - Dividend schedules +│ ├── OptionSpotDataManager - Option contract prices +│ └── MarketTimeseries - Central data repository +│ +└── Derived Metrics Layer + ├── ForwardDataManager - Forward price computation + ├── VolDataManager - Implied volatility calculation + ├── GreekDataManager - Option sensitivities + └── TheoDataFunctions - Theoretical pricing +``` + +### Key Features by Layer + +**BaseDataManager** provides: +- Cache management (CustomCache) +- Key construction (namespaced, artifact-based) +- Configuration (OptionDataConfig singleton) +- Logger setup + +**Market Data Layer** handles: +- Data retrieval from external sources +- Split adjustment handling +- Corporate action processing +- Historical and real-time access + +**Derived Metrics Layer** computes: +- Forward prices using cost-of-carry models +- Implied volatilities via model inversion +- Option greeks using analytical/numerical methods +- Theoretical prices and scenario analysis + +--- + +## Core Data Managers + +### SpotDataManager + +Manages underlying equity spot prices with split adjustment support. + +**Features:** +- Singleton pattern (per symbol) +- 45-day cache expiration +- Split-adjusted (chain_spot) and unadjusted (spot) prices +- Data source: MarketTimeseries (OpenBB/YFinance) + +**Key Methods:** + +```python +# Historical timeseries +result = spot_mgr.get_spot_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + undo_adjust=True # True for split-adjusted, False for raw +) +prices = result.daily_spot # pd.Series with DatetimeIndex + +# Single date +result = spot_mgr.get_at_time(date="2025-01-15") + +# Real-time +result = spot_mgr.rt() +``` + +### RatesDataManager + +Manages risk-free interest rates from US Treasury bills (^IRX). + +**Features:** +- Singleton pattern (global, no symbol) +- 30-day cache expiration +- Data source: YFinance (13-week T-Bill) +- Automatic forward fill for missing dates + +**Key Methods:** + +```python +# Historical rates +result = rates_mgr.get_risk_free_rate_timeseries( + start_date="2025-01-01", + end_date="2025-01-31" +) +rates = result.daily_risk_free_rates # pd.Series (annualized) + +# Real-time +rate = rates_mgr.rt() +``` + +### DividendDataManager + +Manages dividend data with schedule construction for option pricing. + +**Features:** +- Singleton pattern (per symbol) +- 60-day cache expiration + temp cache +- Supports discrete (schedule) and continuous (yield) models +- Handles split adjustments +- Smart partial caching with merging + +**Key Methods:** + +```python +# Get dividend schedules for option pricing +result = div_mgr.get_schedule_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + maturity_date="2025-06-20", + dividend_type=DivType.DISCRETE, + undo_adjust=True +) +schedules = result.daily_discrete_dividends # pd.Series of Schedule objects + +# Real-time dividend schedule +result = div_mgr.rt(maturity_date="2025-06-20") +``` + +### ForwardDataManager + +Computes forward prices using cost-of-carry models. + +**Features:** +- Singleton pattern (per symbol) +- 30-day cache expiration +- Dependencies: SpotDataManager, RatesDataManager, DividendDataManager +- Discrete model: F = S × exp(r×T) - PV(dividends) +- Continuous model: F = S × exp((r-q)×T) + +**Key Methods:** + +```python +# Compute forward prices +result = fwd_mgr.get_forward_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + maturity_date="2025-06-20", + dividend_type=DivType.DISCRETE, + use_chain_spot=True +) +forwards = result.daily_discrete_forward # pd.Series + +# Real-time forward +result = fwd_mgr.rt(maturity_date="2025-06-20") +``` + +### OptionSpotDataManager + +Retrieves option contract market prices from ThetaData API. + +**Features:** +- Not singleton (per symbol) +- 7-day cache expiration +- Data source: ThetaData (EOD or Quote endpoint) +- Returns OHLC data + +**Key Methods:** + +```python +# Historical option prices +result = opt_mgr.get_option_spot_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call", + endpoint_source=OptionSpotEndpointSource.EOD +) +ohlc = result.daily_option_spot # pd.DataFrame [open, high, low, close] + +# Real-time option price +result = opt_mgr.rt(strike=150.0, expiration="2025-06-20", right="call") +``` + +### MarketTimeseries + +Central market data repository with lazy loading and caching. + +**Features:** +- Singleton pattern (global instance) +- Multi-tier caching (memory + disk) +- Data sources: OpenBB, ThetaData, YFinance +- Loads all data for a symbol on first request +- Thread-safe access +- Point-in-time snapshots + +**Usage:** + +```python +from trade.datamanager.vars import get_times_series + +ts = get_times_series() +ts.load("AAPL") # Lazy load on first access + +# Get point-in-time data +data = ts.get_at_index("AAPL", "2025-01-15") +spot = data.spot # pd.Series with OHLCV +``` + +--- + +## Derived Metrics Managers + +### VolDataManager + +Computes implied volatilities from option market prices. + +**Features:** +- Singleton pattern (per symbol) +- 7-day cache expiration +- Multiple models: BSM, CRR binomial, European equivalent +- Supports American and European exercise +- Automatic data loading + +**Key Methods:** + +```python +# Compute implied volatilities +result = vol_mgr.get_implied_volatility_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call", + market_model=OptionPricingModel.BSM, + american=False, + dividend_type=DivType.DISCRETE +) +ivs = result.timeseries # pd.Series of implied vols + +# Real-time IV +iv = vol_mgr.rt(strike=150.0, expiration="2025-06-20", right="call") +``` + +**Pricing Models:** +- `BSM`: Black-Scholes-Merton (fast, European only) +- `BINOMIAL`: Cox-Ross-Rubinstein tree (supports American) +- `EURO_EQIV`: European equivalent IV + +### GreekDataManager + +Computes option sensitivities (delta, gamma, vega, theta, rho). + +**Features:** +- Singleton pattern (per symbol) +- 7-day cache expiration +- Models: BSM (analytical), Binomial (numerical) +- Selectable greeks to reduce computation +- Returns DataFrame with all greeks + +**Key Methods:** + +```python +# Compute option greeks +result = greek_mgr.get_greeks_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call", + greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.VEGA] +) +greeks = result.timeseries # pd.DataFrame with [delta, gamma, vega, ...] + +# Real-time greeks +result = greek_mgr.rt(strike=150.0, expiration="2025-06-20", right="call") +delta = result.timeseries["delta"].iloc[0] +``` + +**Available Greeks:** +- `DELTA`: Rate of change of option price with respect to spot +- `GAMMA`: Rate of change of delta with respect to spot +- `VEGA`: Sensitivity to volatility changes +- `THETA`: Time decay +- `RHO`: Sensitivity to interest rate changes +- `VOLGA`: Vomma (sensitivity of vega to volatility) +- `VANNA`: Sensitivity of delta to volatility + +### Theoretical Pricing Functions + +Module-level functions for option pricing and scenario analysis. + +```python +from trade.datamanager.theo import get_option_theoretical_price, calculate_scenarios + +# Theoretical pricing +theo_result = get_option_theoretical_price( + symbol="AAPL", + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call", + market_model=OptionPricingModel.BSM, + dividend_type=DivType.DISCRETE +) +prices = theo_result.timeseries + +# Scenario analysis (stress testing) +scenarios = calculate_scenarios( + symbol="AAPL", + as_of="2025-01-15", + strike=150.0, + expiration="2025-06-20", + right="call", + spot_scenarios=[0.95, 1.0, 1.05], # -5%, 0%, +5% spot moves + vol_scenarios=[-0.05, 0.0, 0.05], # -5, 0, +5 vol points + return_pnl=True +) +grid = scenarios.grid # pd.DataFrame with spot × vol grid +``` + +--- + +## Unified Timeseries Interface + +The `TimeseriesDataManager` provides a consistent API across all data types. + +### Features + +- **Standardized methods**: `rt()`, `get_at_time()`, `get_timeseries()` +- **Preserves original signatures**: Full docstrings and type hints +- **Property-based access**: `ts.spot`, `ts.vol`, `ts.greeks`, etc. +- **Pass-through to underlying**: Access specialized methods when needed + +### Usage + +```python +from trade.datamanager.timeseries import TimeseriesDataManager + +ts = TimeseriesDataManager("AAPL") + +# Spot data - simple interface +spot_rt = ts.spot.rt() +spot_hist = ts.spot.get_timeseries(start_date="2025-01-01", end_date="2025-01-31") + +# Options data - pass parameters explicitly +vol = ts.vol.get_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="call" +) + +greeks = ts.greeks.rt(strike=150.0, expiration="2025-06-20", right="call") + +# Access underlying manager if needed +vol_mgr = ts.vol._manager +``` + +### Available Properties + +| Property | Manager | Methods Available | +|----------|---------|-------------------| +| `.spot` | SpotDataManager | rt, get_at_time, get_timeseries | +| `.vol` | VolDataManager | rt, get_at_time, get_timeseries | +| `.greeks` | GreekDataManager | rt, get_at_time, get_timeseries | +| `.forward` | ForwardDataManager | rt, get_timeseries | +| `.dividend` | DividendDataManager | rt, get_timeseries | +| `.rates` | RatesDataManager | rt, get_timeseries | +| `.option_spot` | OptionSpotDataManager | rt, get_at_time, get_timeseries | + +--- + +## Convenience Loaders + +### load_full_option_data() + +One-call function to load complete option data packages. + +```python +from trade.datamanager.loaders import load_full_option_data +from trade.datamanager._enums import DivType + +# Load all required data in one call +pack = load_full_option_data( + symbol="AAPL", + strike=150.0, + expiration="2025-06-20", + right="call", + start_date="2025-01-01", + end_date="2025-01-31", + dividend_type=DivType.DISCRETE +) + +# Access all components +spot = pack.spot.timeseries # Spot prices +forward = pack.forward.timeseries # Forward prices +dividend = pack.dividend.timeseries # Dividend schedules +rates = pack.rates.timeseries # Risk-free rates +option = pack.option_spot.timeseries # Market option prices +vol = pack.vol.timeseries # Implied volatilities +greeks = pack.greek.timeseries # Option greeks (DataFrame) +``` + +**Modes:** +- **Timeseries**: Provide `start_date` and `end_date` +- **Single date**: Provide `as_of` +- **Real-time**: Set `rt=True` + +--- + +## Configuration + +### OptionDataConfig (Singleton) + +Global configuration for all data managers. + +```python +from trade.datamanager.config import OptionDataConfig + +config = OptionDataConfig() + +# Modify settings +config.option_model = OptionPricingModel.BSM +config.dividend_type = DivType.DISCRETE +config.n_steps = 200 # Binomial tree steps +config.undo_adjust = True # Use split-adjusted prices +``` + +**Key Settings:** + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `option_spot_endpoint_source` | OptionSpotEndpointSource | EOD | Data source for option prices | +| `dividend_type` | DivType | DISCRETE | Dividend model (DISCRETE/CONTINUOUS) | +| `option_model` | OptionPricingModel | BSM | Pricing model | +| `volatility_model` | VolatilityModel | MARKET | Vol source (MARKET/MODEL_DYNAMIC) | +| `n_steps` | int | 100 | Binomial tree steps | +| `undo_adjust` | bool | True | Use split-adjusted prices | +| `model_price` | ModelPrice | MIDPOINT | Price type (MIDPOINT/BID/ASK/etc.) | +| `real_time_fallback_option` | RealTimeFallbackOption | USE_LAST_AVAILABLE | Fallback for missing RT data | + +### Key Enumerations + +**DivType** (from optionlib): +- `DISCRETE`: Schedule-based dividends +- `CONTINUOUS`: Yield-based dividends + +**OptionPricingModel**: +- `BSM`: Black-Scholes-Merton (fast, European) +- `BINOMIAL`: CRR tree (slower, American) +- `EURO_EQIV`: European equivalent + +**VolatilityModel**: +- `MARKET`: Implied from market prices +- `MODEL_DYNAMIC`: Computed from model + +**GreekType**: +- `DELTA`, `GAMMA`, `VEGA`, `THETA`, `RHO`, `VOLGA`, `VANNA` + +--- + +## Caching System + +### Three-Tier Architecture + +1. **Memory Cache**: Fastest access, per-manager instance, cleared on exit +2. **Disk Cache**: Persistent across sessions, configurable expiration +3. **Partial Merging**: Detects missing dates, fetches only gaps, merges with existing + +### Cache Configuration + +Each manager defines cache behavior via `CacheSpec`: + +```python +from trade.datamanager.base import CacheSpec + +CACHE_SPEC = CacheSpec( + base_dir=DM_GEN_PATH, # Cache directory + cache_fname="spot_data_manager", # Cache filename + default_expire_days=45, # Full cache expiration + clear_on_exit=False # Auto-clear on exit +) +``` + +### Cache Keys + +Constructed using `construct_cache_key()` utility: + +**Components:** +- Symbol (e.g., "AAPL") +- Artifact type (SPOT, IV, GREEKS, etc.) +- Series ID (HIST, AT_TIME, SNAPSHOT) +- Interval (EOD, INTRADAY, NA) +- Optional namespace +- Additional metadata (strike, expiration, model, etc.) + +**Example Keys:** +``` +AAPL__hist__eod__spot__undo_True +AAPL__hist__eod__iv__K150.0_exp20250620_rc_model_bsm +``` + +--- + +## Best Practices + +### ✅ DO + +- **Use singleton managers** - They cache internally, avoid duplicate instances +- **Use `to_datetime()`** for all date conversions (from `trade.helpers.helper`) +- **Provide `DividendsResult`** to `ForwardDataManager` to avoid re-fetching +- **Use `undo_adjust=True`** for backtesting (split-adjusted prices) +- **Specify `greeks_to_compute`** to reduce computation time +- **Use `model_price=MIDPOINT`** for fair value calculations +- **Check `result.is_empty()`** before using data +- **Use `rt()` methods** for real-time data +- **Let managers handle data loading** automatically + +### ❌ DON'T + +- **Don't create multiple instances** of same symbol manager +- **Don't use `datetime.strptime()`** or `pd.to_datetime()` directly +- **Don't mix `undo_adjust=True/False`** in same calculation +- **Don't ignore `dividend_type`** when comparing prices +- **Don't call `_private_methods()`** directly +- **Don't modify `OptionDataConfig`** after initialization +- **Don't assume cache is warm** - check for empty results +- **Don't use BSM model** for American options pricing + +### Common Issues + +| Issue | Solution | +|-------|----------| +| "Data not available" | Check date range with `is_available_on_date()` | +| "Cache miss" | Normal on first run, subsequent runs hit cache | +| "IV solver failed" | Option may be deep ITM/OTM or have bad data | +| "Mismatched undo_adjust" | Ensure consistent split adjustment across managers | + +### Date Handling (CRITICAL) + +**Always use** `to_datetime` from `trade.helpers.helper`: + +```python +from trade.helpers.helper import to_datetime + +# Handles strings, datetime objects, and iterables +date_obj = to_datetime("2025-01-15") +dates = to_datetime(["2025-01-15", "2025-01-16"]) +``` + +**Never use:** +- `datetime.strptime()` +- `pd.to_datetime()` directly + +--- + +## Complete Example: Option Backtesting Workflow + +```python +from trade.datamanager import ( + SpotDataManager, VolDataManager, GreekDataManager +) +from trade.datamanager.timeseries import TimeseriesDataManager +from trade.datamanager.loaders import load_full_option_data +from trade.datamanager._enums import DivType, OptionPricingModel, GreekType + +# Parameters +symbol = "AAPL" +start, end = "2025-01-01", "2025-01-31" +strike, expiration, right = 150.0, "2025-06-20", "call" + +# Method 1: Using individual managers (granular control) +spot_mgr = SpotDataManager(symbol) +vol_mgr = VolDataManager(symbol) +greek_mgr = GreekDataManager(symbol) + +spot_result = spot_mgr.get_spot_timeseries(start, end, undo_adjust=True) +vol_result = vol_mgr.get_implied_volatility_timeseries( + start, end, strike, expiration, right, + market_model=OptionPricingModel.BSM, + dividend_type=DivType.DISCRETE +) +greek_result = greek_mgr.get_greeks_timeseries( + start, end, strike, expiration, right, + greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.VEGA] +) + +# Method 2: Using unified interface (simplified) +ts = TimeseriesDataManager(symbol) +spot_result = ts.spot.get_timeseries(start, end) +vol_result = ts.vol.get_timeseries(start, end, strike, expiration, right) +greek_result = ts.greeks.get_timeseries(start, end, strike, expiration, right) + +# Method 3: One-call loader (most convenient) +pack = load_full_option_data( + symbol=symbol, + strike=strike, + expiration=expiration, + right=right, + start_date=start, + end_date=end, + dividend_type=DivType.DISCRETE +) + +# Access results +spots = pack.spot.timeseries +vols = pack.vol.timeseries +greeks_df = pack.greek.timeseries # DataFrame with delta, gamma, vega, etc. + +# Run scenario analysis +from trade.datamanager.theo import calculate_scenarios + +scenarios = calculate_scenarios( + symbol=symbol, + as_of="2025-01-15", + strike=strike, + expiration=expiration, + right=right, + spot_scenarios=[0.95, 1.0, 1.05], # ±5% spot moves + vol_scenarios=[-0.05, 0.0, 0.05], # ±5 vol points + return_pnl=True +) + +print(scenarios.grid) +``` + +--- + +## Module Structure + +``` +trade/datamanager/ +├── __init__.py # Public API exports +├── base.py # BaseDataManager, CacheSpec +├── config.py # OptionDataConfig singleton +├── result.py # Result dataclasses +├── _enums.py # Enumerations +│ +├── spot.py # SpotDataManager +├── rates.py # RatesDataManager +├── dividend.py # DividendDataManager +├── forward.py # ForwardDataManager +├── option_spot.py # OptionSpotDataManager +├── market_data.py # MarketTimeseries +│ +├── vol.py # VolDataManager +├── greeks.py # GreekDataManager +├── theo.py # Theoretical pricing functions +│ +├── timeseries.py # TimeseriesDataManager, TimeseriesAdapter +├── loaders.py # load_full_option_data() +│ +├── utils/ +│ ├── model.py # Model data loading +│ ├── vol_helpers.py # Volatility calculation +│ ├── greeks_helpers.py # Greek calculation +│ ├── date.py # Date utilities +│ ├── cache.py # Cache utilities +│ ├── data_structure.py # Data validation +│ ├── logging.py # Logging configuration +│ └── enums_utils.py # Cache key construction +│ +└── market_data_helpers/ + └── spot.py # Spot price loading helpers +``` + +--- + +## Contributing + +To add a new manager: + +1. Inherit from `BaseDataManager` +2. Define `CACHE_NAME` (unique string) +3. Define `CACHE_SPEC` (CacheSpec instance) +4. Define `DEFAULT_SERIES_ID` (SeriesId enum) +5. Implement `__init__` with singleton pattern if needed +6. Add methods returning Result subclass +7. Use `self.cache.get()` / `self.cache.set()` for caching +8. Use `construct_cache_key()` for key generation + +**Example skeleton:** + +```python +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager._enums import SeriesId + +class MyDataManager(BaseDataManager): + CACHE_NAME: str = "my_data_manager" + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + DEFAULT_SERIES_ID: SeriesId = SeriesId.HIST + + def __init__(self, symbol: str): + super().__init__(symbol=symbol) + self.symbol = symbol + + def get_my_data(self, start, end) -> MyResult: + key = construct_cache_key(...) + cached = self.cache.get(key) + if cached: + return cached + + # Fetch data + result = MyResult(...) + self.cache.set(key, result) + return result +``` + +--- + +## License + +See main project LICENSE file. + +## Support + +For issues and questions, please refer to the main QuantTools repository. diff --git a/trade/datamanager/__init__.py b/trade/datamanager/__init__.py index e69de29..e36858b 100644 --- a/trade/datamanager/__init__.py +++ b/trade/datamanager/__init__.py @@ -0,0 +1,226 @@ +"""QuantTools DataManager - Comprehensive market data infrastructure for options trading. + +This module provides a complete suite of data managers for retrieving, caching, and +computing market data and derived metrics for quantitative options trading and backtesting. + +Key Components +-------------- + +**Market Data Managers:** + - SpotDataManager: Underlying equity spot prices with split adjustment + - RatesDataManager: Risk-free interest rates from Treasury bills + - DividendDataManager: Dividend schedules (discrete/continuous models) + - OptionSpotDataManager: Option contract market prices from ThetaData + - MarketTimeseries: Central data repository with lazy loading + +**Derived Metrics Managers:** + - ForwardDataManager: Forward price computation using cost-of-carry models + - VolDataManager: Implied volatility calculation (BSM, Binomial) + - GreekDataManager: Option sensitivities (delta, gamma, vega, theta, rho) + +**Unified Interfaces:** + - TimeseriesDataManager: Consistent API across all managers (rt, get_at_time, get_timeseries) + - loaders.load_full_option_data(): One-call comprehensive data loading + +**Utilities:** + - BaseDataManager: Abstract base with caching, configuration, logging + - Result classes: Type-safe containers (SpotResult, VolatilityResult, GreekResultSet, etc.) + - CacheSpec: Cache configuration with expiration control + - Theoretical pricing: get_option_theoretical_price(), calculate_scenarios() + +Design Features +--------------- + +- **Singleton pattern** per symbol for efficient resource management +- **Multi-tier caching** (memory + disk) with intelligent expiration +- **Automatic data loading** from multiple sources (ThetaData, OpenBB, YFinance) +- **Split adjustment handling** for accurate backtesting +- **Type-safe results** with full metadata preservation +- **Consistent API** across all managers via adapter pattern + +Quick Start Examples +-------------------- + +**Individual Manager Usage:** + >>> from trade.datamanager import SpotDataManager, VolDataManager, GreekDataManager + >>> + >>> # Load spot prices + >>> spot_mgr = SpotDataManager("AAPL") + >>> spot_result = spot_mgr.get_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... undo_adjust=True + ... ) + >>> prices = spot_result.daily_spot + >>> + >>> # Compute implied volatilities + >>> vol_mgr = VolDataManager("AAPL") + >>> vol_result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call" + ... ) + >>> ivs = vol_result.timeseries + +**Unified Interface (Recommended):** + >>> from trade.datamanager import TimeseriesDataManager + >>> + >>> # Single entry point for all data types + >>> ts = TimeseriesDataManager("AAPL") + >>> + >>> # Consistent interface across managers + >>> spot = ts.spot.get_timeseries(start_date="2025-01-01", end_date="2025-01-31") + >>> vol = ts.vol.get_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call" + ... ) + >>> greeks = ts.greeks.rt(strike=150.0, expiration="2025-06-20", right="call") + +**One-Call Data Loading:** + >>> from trade.datamanager.loaders import load_full_option_data + >>> from trade.datamanager._enums import DivType + >>> + >>> # Load all option data (spot, forward, dividend, vol, greeks, rates) + >>> pack = load_full_option_data( + ... symbol="AAPL", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... dividend_type=DivType.DISCRETE + ... ) + >>> + >>> # Access all components + >>> spot = pack.spot.timeseries + >>> vol = pack.vol.timeseries + >>> greeks = pack.greek.timeseries + +**Scenario Analysis:** + >>> from trade.datamanager import calculate_scenarios + >>> + >>> # Run stress tests on option position + >>> scenarios = calculate_scenarios( + ... symbol="AAPL", + ... as_of="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... spot_scenarios=[0.95, 1.0, 1.05], # ±5% spot moves + ... vol_scenarios=[-0.05, 0.0, 0.05], # ±5 vol points + ... return_pnl=True + ... ) + >>> print(scenarios.grid) # pd.DataFrame with spot × vol grid + +Configuration +------------- + +Global settings via OptionDataConfig singleton: + >>> from trade.datamanager.config import OptionDataConfig + >>> from trade.datamanager._enums import OptionPricingModel, DivType + >>> + >>> config = OptionDataConfig() + >>> config.option_model = OptionPricingModel.BSM + >>> config.dividend_type = DivType.DISCRETE + >>> config.n_steps = 200 # Binomial tree steps + +Important Notes +--------------- + +**Date Conversion:** + Always use `to_datetime()` from `trade.helpers.helper` for date conversions. + Never use `datetime.strptime()` or `pd.to_datetime()` directly. + +**Split Adjustment:** + Use `undo_adjust=True` for backtesting to get split-adjusted prices. + Ensure consistent `undo_adjust` across all managers in same calculation. + +**Caching:** + Managers use singleton pattern per symbol - avoid creating duplicate instances. + Cache is automatically managed with configurable expiration. + +**Real-time Data:** + Use `.rt()` methods for real-time/latest data. + Configure fallback behavior via `OptionDataConfig.real_time_fallback_option`. + +See Also +-------- + +For comprehensive documentation, see: + - trade/datamanager/README.md: Complete module guide + - Individual manager docstrings: Detailed API documentation + - trade/datamanager/loaders.py: Convenience loader functions + - trade/datamanager/timeseries.py: Unified interface documentation + +Module Structure +---------------- + + datamanager/ + ├── Market Data Layer + │ ├── spot.py - SpotDataManager + │ ├── rates.py - RatesDataManager + │ ├── dividend.py - DividendDataManager + │ ├── option_spot.py - OptionSpotDataManager + │ └── market_data.py - MarketTimeseries + │ + ├── Derived Metrics Layer + │ ├── forward.py - ForwardDataManager + │ ├── vol.py - VolDataManager + │ ├── greeks.py - GreekDataManager + │ └── theo.py - Theoretical pricing functions + │ + ├── Unified Interfaces + │ ├── timeseries.py - TimeseriesDataManager + │ └── loaders.py - load_full_option_data() + │ + ├── Core Infrastructure + │ ├── base.py - BaseDataManager, CacheSpec + │ ├── config.py - OptionDataConfig + │ ├── result.py - Result dataclasses + │ ├── _enums.py - Enumerations + │ └── vars.py - Global instances + │ + └── utils/ - Helper utilities +""" + +from .dividend import DividendDataManager +from .forward import ForwardDataManager +from .rates import RatesDataManager +from .option_spot import OptionSpotDataManager +from .spot import SpotDataManager +from .base import BaseDataManager, CacheSpec +from .vol import VolDataManager +from .greeks import GreekDataManager +from .result import Result, SpotResult, ForwardResult, DividendsResult, RatesResult, OptionSpotResult +from .timeseries import TimeseriesDataManager +from .market_data import MarketTimeseries +from .theo import get_option_theoretical_price, calculate_scenarios +from .utils.model import assert_synchronized_model + +__all__ = [ + "DividendDataManager", + "ForwardDataManager", + "RatesDataManager", + "OptionSpotDataManager", + "SpotDataManager", + "BaseDataManager", + "Result", + "SpotResult", + "ForwardResult", + "DividendsResult", + "RatesResult", + "OptionSpotResult", + "MarketTimeseries", + "TimeseriesDataManager", + "CacheSpec", + "VolDataManager", + "GreekDataManager", + "assert_synchronized_model", + "get_option_theoretical_price", + "calculate_scenarios", +] diff --git a/trade/datamanager/_enums.py b/trade/datamanager/_enums.py new file mode 100644 index 0000000..0966a36 --- /dev/null +++ b/trade/datamanager/_enums.py @@ -0,0 +1,98 @@ +from enum import Enum +from typing import Literal, get_args +from trade.optionlib.config.types import DivType # noqa + +class Interval(str, Enum): + INTRADAY = "intraday" # historical intraday timestamp + EOD = "eod" # end-of-day daily snapshot + NA = "na" # not applicable + +class RealTimeFallbackOption(str, Enum): + RAISE_ERROR = "raise_error" + USE_LAST_AVAILABLE = "use_last_available" + ZEROED = "zeroed" + NAN = "nan" +class SeriesId(str, Enum): + HIST = "hist" + AT_TIME = "at_time" + SNAPSHOT = "snapshot" + +class ArtifactType(str, Enum): + # Market / inputs + SPOT = "spot" + CHAIN = "chain" + RATES = "rates" + DIVS = "divs" + FWD = "forward" + OPTION_SPOT = "option_spot" + DATES = "dates" + + # Volatility + IV = "iv" + TVAR = "tvar" + + # Greeks + GREEKS = "greeks" + DELTA = "delta" + GAMMA = "gamma" + VEGA = "vega" + THETA = "theta" + VOLGA = "volga" + VANNA = "vanna" + RHO = "rho" + +class GreekType(str, Enum): + GREEKS = "greeks" + DELTA = "delta" + GAMMA = "gamma" + VEGA = "vega" + THETA = "theta" + VOLGA = "volga" + VANNA = "vanna" + RHO = "rho" + +class OptionSpotEndpointSource(Enum): + """ + Thetadata creates a native EOD report every day by 6pm ET. + This enum allows choosing between using that EOD report or the intraday quote end point. + This is essential because during market hours, the EOD report is not yet available. + """ + + EOD = "eod" + QUOTE = "quote" + +class ModelPrice(Enum): + """Enumeration of model price type.""" + + MIDPOINT = "midpoint" + BID = "bid" + ASK = "ask" + OPEN = "open" + CLOSE = "close" + + + +class OptionPricingModel(Enum): + """Enumeration of option pricing model.""" + + BSM = "Black-Scholes" + BINOMIAL = "Binomial" + EURO_EQIV = "European Equivalent" + + +class VolatilityModel(Enum): + """Enumeration of volatility model.""" + + MARKET = "market" + MODEL_DYNAMIC = "model_dynamic" + + +GREEKS = Literal[ + GreekType.DELTA.value, + GreekType.GAMMA.value, + GreekType.THETA.value, + GreekType.VEGA.value, + GreekType.RHO.value, + GreekType.VOLGA.value, +] +AVAILABLE_GREEKS = get_args(GREEKS) \ No newline at end of file diff --git a/trade/datamanager/base.py b/trade/datamanager/base.py new file mode 100644 index 0000000..f4712b5 --- /dev/null +++ b/trade/datamanager/base.py @@ -0,0 +1,231 @@ +from abc import ABC +from dataclasses import dataclass +from typing import Any, Callable, ClassVar, Dict, Optional, Type, TypeVar +from trade.datamanager.config import OptionDataConfig +from trade.datamanager.utils.logging import get_logging_level +from trade.helpers.helper import CustomCache +from trade.helpers.Logging import setup_logger +from pathlib import Path +from .vars import DM_GEN_PATH +from ._enums import Interval, ArtifactType, SeriesId +from .utils.enums_utils import construct_cache_key +logger = setup_logger("trade.datamanager.base", stream_log_level=get_logging_level()) + +# Assumes you already have these (from your cache_key module) +# from cache_key import construct_cache_key, Interval, ArtifactType, SeriesId + +T = TypeVar("T") + + +# REMEBER: Take out the commented out parts +@dataclass(frozen=True, slots=True) +class CacheSpec: + """ + Optional: a small config object you can pass around, so all managers + initialize their caches in a consistent way. + + If you already have a cache registry/factory, you may not need this. + + args: + base_dir (Optional[Path]): Directory for cache storage. + default_expire_days (Optional[int]): Default expiration time in days. This is how many days till the entire cache entry expires. + default_expire_seconds (Optional[int]): Default expiration time in seconds. This is how many seconds till a single cache entry expires. + cache_fname (Optional[str]): Foldername for the cache storage. + clear_on_exit (bool): If True, clears the cache on exit. + """ + + base_dir: Optional[Path] = DM_GEN_PATH.as_posix() + default_expire_days: Optional[int] = 500 + default_expire_seconds: Optional[int] = None + cache_fname: Optional[str] = None + clear_on_exit: bool = False + + +class BaseDataManager(ABC): + """ + Foundation class for all DataManagers. + + Goals: + - Every inheritor gets a cache. + - Every inheritor MUST define CACHE_NAME. + - Provide consistent key creation (namespaced). + - Provide thin get/set/get_or_compute wrappers. + - Keep business logic out of the base. + """ + + DEFAULT_INTERVAL: ClassVar[Optional["Interval"]] = None + DEFAULT_SERIES_ID: ClassVar["SeriesId"] # prefer explicit in subclasses + _CACHE_NAME_REGISTRY: ClassVar[Dict[str, Type["BaseDataManager"]]] = {} + CONFIG: OptionDataConfig = OptionDataConfig() + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Enforces that all subclasses define CACHE_NAME and DEFAULT_SERIES_ID.""" + super().__init_subclass__(**kwargs) + + if cls is BaseDataManager: + return + + cache_name = getattr(cls, "CACHE_NAME", None) + cache_spec = getattr(cls, "CACHE_SPEC", None) + + if not isinstance(cache_name, str) or not cache_name.strip(): + raise TypeError(f"{cls.__name__} must define a non-empty class variable CACHE_NAME: str") + + if not isinstance(cache_spec, CacheSpec): + raise TypeError(f"{cls.__name__} must define a class variable CACHE_SPEC of type CacheSpec") + + cache_name = cache_name.strip() + + # Enforce uniqueness to avoid collisions + existing = cls._CACHE_NAME_REGISTRY.get(cache_name) # noqa + # if existing is not None and existing is not cls: + # raise TypeError( + # f"Duplicate CACHE_NAME='{cache_name}'. " + # f"Already used by {existing.__name__}. " + # f"Pick a unique CACHE_NAME for {cls.__name__}." + # ) + + cls._CACHE_NAME_REGISTRY[cache_name] = cls + + # Optional: enforce that DEFAULT_SERIES_ID exists (if you want) + if not hasattr(cls, "DEFAULT_SERIES_ID"): + raise TypeError(f"{cls.__name__} must define DEFAULT_SERIES_ID (e.g., SeriesId.HIST).") + + def __init__( + self, + *, + enable_namespacing: bool = False, + symbol: Optional[str] = None, + ) -> None: + """ + Parameters + ---------- + cache: + Your existing CustomCache instance (diskcache-backed). + enable_namespacing: + If True, keys are prefixed with CACHE_NAME to avoid collisions. + """ + self.cache_spec = self.CACHE_SPEC + self.symbol = symbol + self.cache = CustomCache( + location=self.cache_spec.base_dir, + fname=self.cache_spec.cache_fname, + expire_days=self.cache_spec.default_expire_days, + clear_on_exit=self.cache_spec.clear_on_exit, + ) + self.enable_namespacing = enable_namespacing + out = self.cache.expire() + if out > 0: + logger.info(f"{self.CACHE_NAME} has expired {out} entries") + + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}(symbol={self.symbol}, cache='{self.CACHE_NAME}', all_entries={len(self.cache)})>" + + @classmethod + def get_cache(cls) -> CustomCache: + """Returns the cache instance.""" + + c = CustomCache( + location=cls.CACHE_SPEC.base_dir, + fname=cls.CACHE_SPEC.cache_fname, + expire_days=cls.CACHE_SPEC.default_expire_days, + clear_on_exit=cls.CACHE_SPEC.clear_on_exit, + ) + return c + + @classmethod + def clear_all_caches(cls) -> None: + """Clears caches for all registered DataManager subclasses.""" + for cache_name, manager_cls in cls._CACHE_NAME_REGISTRY.items(): + logger.info(f"Clearing cache for {manager_cls.__name__} (CACHE_NAME='{cache_name}')") + manager_cls.get_cache().clear() + + def clear_cache(self) -> None: + """Clears this DataManager's cache.""" + logger.info(f"Clearing cache for {self.__class__.__name__} (CACHE_NAME='{self.CACHE_NAME}')") + self.cache.clear() + + # Key construction + def make_key( + self, + *, + symbol: str, + interval: Optional[Interval] = None, + artifact_type: ArtifactType, + series_id: Optional[SeriesId] = None, + **extra_parts: Any, + ) -> str: + """ + Namespaced key builder that wraps your construct_cache_key. + + You decided: + - no caching SNAPSHOT series_id (but you might still request it) + - time is explicit if you do AT_TIME + """ + interval = interval if interval is not None else self.DEFAULT_INTERVAL + series_id = series_id if series_id is not None else self.DEFAULT_SERIES_ID + + raw = construct_cache_key( + symbol=symbol, + interval=interval, + artifact_type=artifact_type, + series_id=series_id, + **extra_parts, + ) + + if not self.enable_namespacing: + return raw + + return f"{self.CACHE_NAME}|{raw}" + + # Cache IO + def get(self, key: str, default: Any = None) -> Any: + return self.cache.get(key, default=default) + + def set(self, key: str, value: Any, *, expire: Optional[int] = None) -> None: + if expire is None: + expire = self.cache_spec.default_expire_seconds + self.cache.set(key, value, expire=expire) + + def delete(self, key: str) -> None: + self.cache.delete(key) + + def contains(self, key: str) -> bool: + return key in self.cache + + def cache_it(self, key: str, value: Any, *, expire: Optional[int] = None) -> None: + raise NotImplementedError(f"{self.__class__.__name__}.cache() not implemented.") + + def get_or_compute( + self, + key: str, + compute_fn: Callable[[], T], + *, + expire: Optional[int] = None, + force: bool = False, + ) -> T: + """ + Read-through caching helper. + + force=True bypasses cache read, recomputes and overwrites cache. + """ + if not force: + hit = self.cache.get(key, default=None) + if hit is not None: + return hit # type: ignore[return-value] + + value = compute_fn() + self.set(key, value, expire=expire) + return value + + # Offload hook (cron calls this) + def offload(self, *args: Any, **kwargs: Any) -> None: + """ + Optional standard hook. + + You can override in subclasses or implement a shared offloader that + knows how to iterate keys / export values. Keeping it as a stub here + avoids forcing a storage design too early. + """ + raise NotImplementedError(f"{self.__class__.__name__}.offload() not implemented.") diff --git a/trade/datamanager/config.py b/trade/datamanager/config.py new file mode 100644 index 0000000..e9d1d9d --- /dev/null +++ b/trade/datamanager/config.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +from typing import List, Union +from trade.helpers.helper_types import SingletonMetaClass +from trade.optionlib.config.types import (DiscreteDivGrowthModel, DivType,) +from trade.optionlib.config.defaults import DIVIDEND_LOOKBACK_YEARS +from ._enums import ( + GreekType, + OptionSpotEndpointSource, + OptionPricingModel, + VolatilityModel, + RealTimeFallbackOption, + ModelPrice +) +from typeguard import check_type +from typing import get_type_hints + +@dataclass +class OptionDataConfig(metaclass=SingletonMetaClass): + """Configuration for OptionDataManager.""" + + option_spot_endpoint_source: OptionSpotEndpointSource = OptionSpotEndpointSource.EOD + default_lookback_years: int = DIVIDEND_LOOKBACK_YEARS + default_forecast_method: DiscreteDivGrowthModel = DiscreteDivGrowthModel.CONSTANT + dividend_type: DivType = DivType.DISCRETE + include_special_dividends: bool = False + option_model: OptionPricingModel = OptionPricingModel.BINOMIAL + volatility_model: VolatilityModel = VolatilityModel.MARKET + n_steps: int = 100 + undo_adjust: bool = True + real_time_fallback_option: RealTimeFallbackOption = RealTimeFallbackOption.USE_LAST_AVAILABLE + model_price: ModelPrice = ModelPrice.MIDPOINT + filter_out_special_dividends: bool = True + greeks_to_compute: Union[List[GreekType], GreekType] = GreekType.GREEKS + + + def assert_valid(self) -> None: + """Validates all configuration values against business rules.""" + assert self.default_lookback_years > 0, "Lookback years must be positive." + assert self.default_lookback_years <= 5, "Lookback years seems too large. Max 5." + assert isinstance( + self.default_forecast_method, DiscreteDivGrowthModel + ), "Invalid forecast method. Expected DiscreteDivGrowthModel Enum." + assert isinstance(self.dividend_type, DivType), "Invalid dividend type. Expected DivType Enum." + assert isinstance(self.include_special_dividends, bool), "include_special_dividends must be a boolean." + assert isinstance( + self.option_spot_endpoint_source, OptionSpotEndpointSource + ), "Invalid option_spot_endpoint_source. Expected OptionSpotEndpointSource Enum." + assert isinstance( + self.option_model, OptionPricingModel + ), "Invalid option_model. Expected OptionPricingModel Enum." + assert isinstance( + self.volatility_model, VolatilityModel + ), "Invalid volatility_model. Expected VolatilityModel Enum." + assert isinstance( + self.real_time_fallback_option, RealTimeFallbackOption + ), "Invalid real_time_fallback_option. Expected RealTimeFallbackOption Enum." + assert isinstance(self.n_steps, int) and self.n_steps > 0, "n_steps must be a positive integer." + assert isinstance(self.undo_adjust, bool), "undo_adjust must be a boolean." + assert isinstance(self.model_price, ModelPrice), "Invalid model_price. Expected ModelPrice Enum." + def __post_init__(self) -> None: + """Validates configuration after initialization.""" + self.assert_valid() + + def __setattr__(self, name, value): + """Validates configuration after any attribute change.""" + all_hints = get_type_hints(self.__class__) + hint = all_hints.get(name) + if hint is not None: + check_type(value, hint) + super().__setattr__(name, value) diff --git a/trade/datamanager/dividend.py b/trade/datamanager/dividend.py new file mode 100644 index 0000000..0629c5c --- /dev/null +++ b/trade/datamanager/dividend.py @@ -0,0 +1,703 @@ +"""Dividend data management for options pricing with caching and schedule construction. + +This module provides the DividendDataManager class for retrieving, caching, and +constructing dividend schedules (discrete or continuous) for equity symbols. Supports +backtest-style time-series construction with split adjustments and partial caching. + +Typical usage: + >>> div_mgr = DividendDataManager("AAPL") + >>> result = div_mgr.get_schedule_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... undo_adjust=True + ... ) + >>> schedules = result.daily_discrete_dividends +""" + +from datetime import datetime +from typing import Any, ClassVar, Optional, Tuple, Union, List +import pandas as pd +from trade.helpers.Logging import setup_logger +from trade.optionlib.assets.dividend import Schedule, ScheduleEntry +from trade.datamanager.vars import get_times_series, DM_GEN_PATH, load_name +from trade.datamanager.config import OptionDataConfig +from trade.datamanager.result import DividendsResult +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager._enums import ArtifactType, SeriesId, Interval, RealTimeFallbackOption +from trade.datamanager.utils import slice_schedule +from trade.datamanager.utils.date import DateRangePacket, is_available_on_date +from trade.datamanager.utils.cache import _data_structure_cache_it +from trade.datamanager.utils.logging import get_logging_level +from trade.helpers.helper import CustomCache, get_missing_dates, change_to_last_busday, to_datetime +from trade.optionlib.config.types import DivType +from trade.optionlib.assets.dividend import get_vectorized_dividend_scehdule + +from trade import HOLIDAY_SET +from .utils.data_structure import _data_structure_sanitize + +logger = setup_logger("trade.datamanager.dividend", stream_log_level=get_logging_level()) +TS = get_times_series() + +class DividendDataManager(BaseDataManager): + """Manages dividend data retrieval, caching, and schedule construction for a specific symbol. + + This manager handles both discrete and continuous dividends with intelligent caching, + partial cache merging, and split adjustment logic. Implements singleton pattern per symbol + to avoid redundant timeseries loading. + + Attributes: + CACHE_NAME: Class-level cache identifier for this manager type. + DEFAULT_SERIES_ID: Default historical series identifier. + CONFIG: Configuration object for dividend data settings. + INSTANCES: Class-level cache of manager instances per symbol. + symbol: The equity ticker symbol this manager handles. + temp_cache: Short-lived cache for temporary dividend data. + + Examples: + >>> # Singleton access - same instance returned for same symbol + >>> div_mgr1 = DividendDataManager("AAPL") + >>> div_mgr2 = DividendDataManager("AAPL") + >>> assert div_mgr1 is div_mgr2 + + >>> # Get discrete dividend schedule for a date range + >>> schedule, key = div_mgr1.get_discrete_dividend_schedule( + ... start_date="2025-01-01", + ... end_date="2025-06-20" + ... ) + + >>> # Get daily time-series of schedules + >>> result = div_mgr1.get_schedule_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20" + ... ) + """ + + CACHE_NAME: ClassVar[str] = "dividend_data_manager" + DEFAULT_SERIES_ID: ClassVar["SeriesId"] = SeriesId.HIST + CONFIG = OptionDataConfig() + INSTANCES = {} + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + + def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> "DividendDataManager": + """Returns cached instance for symbol, creating new one if needed. + + Implements singleton pattern per symbol to ensure timeseries are loaded only once. + Automatically loads market timeseries data on first instantiation. + + Args: + symbol: Equity ticker symbol (e.g., "AAPL", "MSFT"). + *args: Additional positional arguments passed to __init__. + **kwargs: Additional keyword arguments passed to __init__. + + Returns: + Singleton DividendDataManager instance for the given symbol. + + Examples: + >>> mgr1 = DividendDataManager("AAPL") + >>> mgr2 = DividendDataManager("AAPL") + >>> assert mgr1 is mgr2 # Same instance + """ + if symbol not in cls.INSTANCES: + instance = object.__new__(cls) + cls.INSTANCES[symbol] = instance + return cls.INSTANCES[symbol] + + def __init__( + self, symbol: str, *, enable_namespacing: bool = False + ) -> None: + """Initializes manager for a symbol with cache and temp cache for short-lived data. + + Sets up persistent cache for dividend schedules and temporary cache for short-lived + data. Only executes once per symbol due to singleton pattern. + + Args: + symbol: Equity ticker symbol. + enable_namespacing: If True, enables namespace isolation in cache keys. + + Examples: + >>> mgr = DividendDataManager("AAPL") + >>> mgr = DividendDataManager("AAPL", enable_namespacing=True) + """ + + if getattr(self, "_initialized", False): + return + self._initialized = True + super().__init__(enable_namespacing=enable_namespacing, symbol=symbol) + self.symbol = symbol + self.temp_cache: CustomCache = CustomCache( + location=DM_GEN_PATH.as_posix(), fname="dividend_temp_cache", expire_days=1, clear_on_exit=True + ) + + ## General caching logic + def cache_it(self, key: str, value: Any, *, expire: Optional[int] = None, _type: str = "discrete") -> None: + """Caches dividend data with merge logic for discrete dividends (no future dates). + + For discrete dividends, implements smart merging: filters out future dates (> today) + and merges with existing cache by date, keeping unique entries. For other types, + performs direct cache storage. + + Args: + key: Cache key identifier. + value: Data to cache (typically List[ScheduleEntry] for discrete). + expire: Optional expiration time in seconds. Uses cache default if None. + _type: Type of dividend data ("discrete" or other). Affects merge logic. + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> schedule = [ScheduleEntry(date=date(2025, 3, 15), amount=0.25)] + >>> div_mgr.cache_it("my_key", schedule, expire=86400, _type="discrete") + + Notes: + - Discrete dividends are filtered to exclude future dates (> today) + - Existing cache entries are merged and deduplicated by date + - Non-discrete types bypass merge logic + """ + + ## If discrete dividends, we first check if key exists + ## If it does, we add to it. Only values <= today. + ## If it does not, we create new entry + if _type == "discrete": + existing = self.get(key, default=None) + today = datetime.today().date() + allowed = [e for e in value if e.date <= today] + + if existing is not None: + # Merge existing and new values. We're expecting lists of ScheduleEntry + merged = existing + allowed + + ## Unique by date + merged = {entry.date: entry for entry in merged} + uniques = sorted(merged.values(), key=lambda e: e.date) + self.set(key, uniques, expire=expire) + return + else: + self.set(key, allowed, expire=expire) + return + + # For other types or if no existing, just setattr + self.set(key, value, expire=expire) + + ## Dividend yield history retrieval for continuous dividends. Already cached in MarketTimeseries. + def get_div_yield_history(self, symbol: str) -> pd.Series: + """Retrieves continuous dividend yield history from MarketTimeseries. + + Fetches annual dividend yield as a percentage time-series from the global + MarketTimeseries cache (TS). Used for continuous dividend modeling. + + Args: + symbol: Equity ticker symbol. + + + Returns: + Time-indexed Series of annualized dividend yields (e.g., 0.025 = 2.5%). + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> yields = div_mgr.get_div_yield_history("AAPL") + >>> logger.info(yields.head()) + datetime + 2020-01-02 0.0124 + 2020-01-03 0.0125 + ... + """ + div_history = TS._get_dividend_yield_timeseries(symbol) + return div_history + ## Discrete dividend schedule retrieval with caching. + def get_discrete_dividend_schedule( + self, + *, + end_date: Union[str, datetime, pd.Timestamp], + start_date: Union[str, datetime, pd.Timestamp], + valuation_date: Optional[Union[str, datetime, pd.Timestamp]] = None, + ) -> Tuple[List[ScheduleEntry], str]: + """Returns discrete dividend schedule between dates with partial cache support. + + Fetches individual dividend payment events (ex-dates and amounts) expected between + start_date and end_date. Intelligently uses cache if available and fetches missing + data only when needed. + + Args: + start_date: Start of date range for dividend events (YYYY-MM-DD string or datetime). + end_date: End of date range for dividend events (YYYY-MM-DD string or datetime). + valuation_date: Optional reference date for forecasting. Defaults to start_date. + + Returns: + Tuple containing: + - List[ScheduleEntry]: Dividend events with dates and amounts + - str: Cache key used for this data + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> schedule, key = div_mgr.get_discrete_dividend_schedule( + ... start_date="2025-01-01", + ... end_date="2025-06-20" + ... ) + >>> for entry in schedule: + ... logger.info(f"{entry.date}: ${entry.amount:.2f}") + 2025-02-14: $0.25 + 2025-05-16: $0.25 + + Notes: + - Uses vectorized dividend schedule fetching from optionlib + - Partial cache hits trigger fetches for missing date ranges only + - Cache stores raw ScheduleEntry lists without splits applied + """ + + ## Load first + load_name(self.symbol) + + ## Dates + packet = DateRangePacket(start_date, end_date) + start_date = packet.start_date + end_date = packet.end_date + start_str = packet.start_str + end_str = packet.end_str + + ticker = self.symbol + method = self.CONFIG.default_forecast_method.value + lookback_years = self.CONFIG.default_lookback_years + key = self.make_key( + symbol=ticker, + artifact_type=ArtifactType.DIVS, + series_id=SeriesId.HIST, + method=method, + lookback_years=lookback_years, + current_state="schedule", + interval=Interval.NA, + vendor="yfinance", + ) + + available_schedule = self.get(key, default=None) + if available_schedule: + logger.info(f"Cache hit for key: {key}") + ## If max date in available schedule >= end_date, we can use cache + max_cached_date = max(entry.date for entry in available_schedule) + min_cached_date = min(entry.date for entry in available_schedule) + fully_covered = (min_cached_date <= to_datetime(start_str).date()) and ( + max_cached_date >= to_datetime(end_str).date() + ) + if fully_covered: + logger.info(f"Cache fully covers requested date range. Key: {key}") + + ## Filter to requested date range + start_dt = to_datetime(start_str).date() + end_dt = to_datetime(end_str).date() + filtered_schedule = [e for e in available_schedule if start_dt <= e.date <= end_dt] + return filtered_schedule, key + else: + logger.info(f"Cache partially covers requested date range. Key: {key}. Fetching missing data.") + + schedule = get_vectorized_dividend_scehdule( + tickers=[ticker], + end_dates=[end_date], + start_dates=[start_date], + method=method, + lookback_years=lookback_years, + valuation_dates=[valuation_date] if valuation_date else None, + ) + + raw_schedule = schedule[0].schedule + self.cache_it(key, raw_schedule, _type="discrete") + + return raw_schedule, key + + ## Switcher to choose between constructing all the way or using cached pieces + def _get_discrete_schedule_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + undo_adjust: bool = True, + ) -> Tuple[pd.Series, str]: + """Builds daily dividend schedule series with partial cache merging and split adjustment. + + Constructs a time-series where each business day gets its own Schedule object representing + dividends from that valuation date to maturity. Optimizes by fetching dividend events once + and slicing for each date. Optionally applies split adjustments. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + maturity_date: Fixed horizon date for all schedules (e.g., option expiry). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Uses config default if None. + undo_adjust: If True, adjusts dividends for splits as of valuation date. + + Returns: + Tuple containing: + - pd.Series: DatetimeIndex with Schedule objects as values + - str: Cache key used + + Raises: + ValueError: If maturity_date < start_date. + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> series, key = div_mgr._get_discrete_schedule_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... undo_adjust=True + ... ) + >>> logger.info(series.head()) + datetime + 2025-01-02 Schedule([ScheduleEntry(...), ...]) + 2025-01-03 Schedule([ScheduleEntry(...), ...]) + ... + + Notes: + - Fetches full schedule from start_date to maturity_date once + - Builds daily schedules by slicing based on valuation date + - Split adjustments multiply dividend amounts by cumulative split factor + - Partial cache hits merge with existing data + - Cache expires after 12 hours + """ + logger.info( + f"Fetching discrete dividend schedule timeseries for {self.symbol} from {start_date} to {end_date} with maturity {maturity_date}" + ) + packet = DateRangePacket(start_date, end_date, maturity_date=maturity_date) + dividend_type = DivType(dividend_type) if dividend_type is not None else self.CONFIG.dividend_type + is_partial = False + start_dt = packet.start_date.date() + end_dt = packet.end_date.date() + mat_dt = packet.maturity_date.date() + start_str = packet.start_str + end_str = packet.end_str + mat_str = packet.maturity_str + + if mat_dt < start_dt: + logger.info(f"Maturity date {mat_dt} is before start date {start_dt}") + raise ValueError("maturity_date must be >= start_date") + + key = self.make_key( + symbol=self.symbol, + artifact_type=ArtifactType.DIVS, + series_id=SeriesId.HIST, + method=self.CONFIG.default_forecast_method.value, + lookback_years=self.CONFIG.default_lookback_years, + current_state="schedule_timeseries", + interval=Interval.EOD, + undo_adjust=undo_adjust, + maturity=mat_str, + ) + + cached_series = self.get(key, default=None) + if cached_series is not None: + logger.info(f"Cache hit for discrete schedule timeseries key: {key}") + missing_dates = get_missing_dates(cached_series, start_str, end_str) + if not missing_dates: + logger.info(f"Cache fully covers requested date range for timeseries. Key: {key}") + cached_series = cached_series[ + (cached_series.index >= pd.to_datetime(start_date)) + & (cached_series.index <= pd.to_datetime(end_date)) + ] + return cached_series, key + else: + logger.info( + f"Cache partially covers requested date range for timeseries. Key: {key}. Fetching missing dates: {missing_dates}" + ) + start_str, end_str = min(missing_dates), max(missing_dates) + is_partial = True + else: + logger.info(f"No cache found for discrete schedule timeseries key: {key}. Building from scratch.") + + # Build from scratch for missing dates + # Fetch ONCE: all events from start_date to maturity_date + full_schedule, _ = self.get_discrete_dividend_schedule( + start_date=start_str, + end_date=mat_str, + valuation_date=start_str, + ) + + # Build daily schedules efficiently using a moving pointer + series = {} + date_range = pd.date_range(start=start_dt, end=end_dt, freq="B").strftime("%Y-%m-%d") + for d in date_range: + if d in HOLIDAY_SET: + # Skip holidays + continue + d_date = datetime.strptime(d, "%Y-%m-%d").date() + + ## Simple filter approach + sliced = slice_schedule(full_schedule, d_date, mat_dt) + series[d_date] = Schedule(sliced) + data = pd.Series(series, name="dividend_schedule") + + # Back-adjust to represent cashflows as of valuation date. Ie undoing splits + if undo_adjust: + data = data.to_frame() + # split_factors = TS._split_factor[self.symbol].copy() + split_factors = TS._get_split_factor_timeseries(sym=self.symbol) + data["split_factor"] = split_factors + data["dividend_schedule"] = data["dividend_schedule"] * data["split_factor"] + data = data["dividend_schedule"] + + # Cache the constructed timeseries + if is_partial: + # Merge with existing cached series + merged = pd.concat([cached_series, data]) + data = merged[~merged.index.duplicated(keep="last")] + + data = _data_structure_sanitize(data, start_date, end_date, source_name=f"discrete_schedule_timeseries for {self.symbol}") + + _data_structure_cache_it(self, key, data, expire=86400) + return data, key + + def get_schedule_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + undo_adjust: bool = True, + ) -> DividendsResult: + """Returns daily dividend schedule time-series from valuation dates to maturity. + + Constructs a daily series where each business day has its own Schedule representing + dividends from that valuation date to the fixed maturity date. Supports both discrete + and continuous dividend models. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + maturity_date: Fixed horizon date for all schedules (e.g., option expiry). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Uses config default if None. + undo_adjust: If True, adjusts dividends for splits as of valuation date. + + Returns: + DividendsResult containing daily_discrete_dividends or daily_continuous_dividends + Series, plus metadata (key, dividend_type, undo_adjust). + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> result = div_mgr.get_schedule_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... undo_adjust=True + ... ) + >>> schedules = result.daily_discrete_dividends + >>> logger.info(schedules.iloc[0]) # First day's schedule + Schedule([ScheduleEntry(date=..., amount=...)]) + + Notes: + - For DISCRETE: Returns Series of Schedule objects (one per day) + - For CONTINUOUS: Returns Series of annual yield percentages + - start_date/end_date define valuation date range + - maturity_date is the fixed horizon (e.g., option expiry) + """ + load_name(self.symbol) + if dividend_type: + logger.info(f"Using provided dividend_type: {dividend_type}") + else: + logger.info(f"Using config default dividend_type: {self.CONFIG.dividend_type}") + + dividend_type = DivType(dividend_type) if dividend_type is not None else self.CONFIG.dividend_type + result = DividendsResult() + result.symbol = self.symbol + result.dividend_type = dividend_type + result.undo_adjust = undo_adjust + + if dividend_type == DivType.DISCRETE: + data, key = self._get_discrete_schedule_timeseries( + start_date=start_date, + end_date=end_date, + maturity_date=maturity_date, + dividend_type=dividend_type, + undo_adjust=undo_adjust, + ) + data.index = pd.to_datetime(data.index) + data.index.name = "datetime" + data = data[(data.index >= pd.to_datetime(start_date)) & (data.index <= pd.to_datetime(end_date))] + data = data.sort_index() + data = data.drop_duplicates() + result.daily_discrete_dividends = data + result.key = key + + elif dividend_type == DivType.CONTINUOUS: + start_str = ( + pd.to_datetime(start_date).strftime("%Y-%m-%d") if isinstance(start_date, datetime) else start_date + ) + end_str = pd.to_datetime(end_date).strftime("%Y-%m-%d") if isinstance(end_date, datetime) else end_date + yield_history = self.get_div_yield_history(self.symbol) + filtered = yield_history[(yield_history.index >= start_str) & (yield_history.index <= end_str)] + filtered.index.name = "datetime" + filtered.index = to_datetime(filtered.index) + filtered = filtered.sort_index() + result.daily_continuous_dividends = filtered + result.key = None + return result + + def get_schedule( + self, + valuation_date: Union[datetime, str], + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + undo_adjust: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + ) -> DividendsResult: + """Returns dividend schedule for a single valuation date to maturity. + + Fetches dividend data (discrete events or continuous yields) from a single + valuation date to maturity date. Suitable for real-time pricing scenarios. + + Args: + valuation_date: Reference date for valuation (YYYY-MM-DD string or datetime). + maturity_date: Horizon date for dividends (YYYY-MM-DD string or datetime). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Uses config default if None. + undo_adjust: If True, adjusts dividends for splits as of valuation date. + + Returns: + DividendsResult with daily_discrete_dividends or daily_continuous_dividends, + plus metadata. + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> result = div_mgr.get_schedule( + ... valuation_date="2025-01-15", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... undo_adjust=True + ... ) + >>> schedule = result.daily_discrete_dividends.iloc[0] + >>> logger.info(schedule.schedule) # List of ScheduleEntry objects + + Notes: + - For DISCRETE: Returns Series with single entry containing Schedule object + - For CONTINUOUS: Returns filtered yield history between dates + - Split adjustments applied if undo_adjust=True + """ + load_name(self.symbol) + fallback_option = fallback_option or self.CONFIG.real_time_fallback_option + dividend_type = DivType(dividend_type) if dividend_type is not None else self.CONFIG.dividend_type + valuation_date = to_datetime(valuation_date) + + if not is_available_on_date(valuation_date): + logger.warning(f"Valuation date {valuation_date} is not a business day or holiday. No dividends available. Resolution: {fallback_option}") + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"Valuation date {valuation_date} is not a business day or holiday.") + if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## Using only change_to_last_busday assumes input date is not business day or is holiday + ## Which the function would roll back + ## But there's a possibility input date is today's date but before market open + ## In that case we need to move back one more business day + valuation_date = change_to_last_busday(valuation_date - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + else: + result = DividendsResult() + dividend = pd.Series(dtype=float) + if dividend_type == DivType.DISCRETE: + result.daily_discrete_dividends = dividend + else: + result.daily_continuous_dividends = dividend + result.key = None + result.undo_adjust = undo_adjust + result.dividend_type = dividend_type + result.symbol = self.symbol + result.fallback_option = fallback_option + return result + + + + + val_str = valuation_date.strftime("%Y-%m-%d") if isinstance(valuation_date, datetime) else valuation_date + mat_str = maturity_date.strftime("%Y-%m-%d") if isinstance(maturity_date, datetime) else maturity_date + + if dividend_type == DivType.DISCRETE: + data, key = self.get_discrete_dividend_schedule( + start_date=val_str, + end_date=mat_str, + valuation_date=val_str, # optional, but consistent + ) + if undo_adjust: + split_factor = TS.get_split_factor_at_index(self.symbol, pd.to_datetime(valuation_date)) + else: + split_factor = 1.0 + data = Schedule(schedule=[entry * split_factor for entry in data]) + data = pd.Series({val_str: data}) + data.index = to_datetime(data.index) + data.index.name = "datetime" + elif dividend_type == DivType.CONTINUOUS: + data = self.get_div_yield_history(self.symbol) + data = data[ + (data.index.date >= pd.to_datetime(valuation_date).date()) + & (data.index.date <= pd.to_datetime(valuation_date).date()) + ] + data.index.name = "datetime" + data.index = to_datetime(data.index) + key = None + else: + raise ValueError(f"Unsupported dividend type: {dividend_type}") + + result = DividendsResult() + + if dividend_type == DivType.DISCRETE: + result.daily_discrete_dividends = data + else: + result.daily_continuous_dividends = data + result.key = key + result.undo_adjust = undo_adjust + result.dividend_type = dividend_type + result.symbol = self.symbol + result.fallback_option = fallback_option + + return result + + def rt( + self, + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + undo_adjust: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + ) -> DividendsResult: + """Real-time enabled method to get dividend schedule for a single valuation date. + + Wrapper around get_schedule with real-time fallback handling. If data is missing + for the valuation date, applies the specified fallback strategy. + + Args: + valuation_date: Reference date for valuation (YYYY-MM-DD string or datetime). + maturity_date: Horizon date for dividends (YYYY-MM-DD string or datetime). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Uses config default if None. + undo_adjust: If True, adjusts dividends for splits as of valuation date. + fallback_option: Strategy for handling missing data. Uses config default if None. + Returns: + DividendsResult with dividend schedule or fallback data. + """ + load_name(self.symbol) + + if fallback_option is None: + fallback_option = self.CONFIG.real_time_fallback_option + + result = self.get_schedule( + valuation_date=datetime.now(), + maturity_date=maturity_date, + dividend_type=dividend_type, + undo_adjust=undo_adjust, + fallback_option=fallback_option, + ) + result.rt = True + return result + + def offload(self, *args: Any, **kwargs: Any) -> None: + + """ + Placeholder for offload logic (not implemented). + + Reserved for future implementation of cache offloading or cleanup operations. + Currently performs no action. + + Args: + *args: Arbitrary positional arguments. + **kwargs: Arbitrary keyword arguments. + + Examples: + >>> div_mgr = DividendDataManager("AAPL") + >>> div_mgr.offload() # No-op + """ + logger.info(f"No offload logic implemented for {self.CACHE_NAME}") + diff --git a/trade/datamanager/exceptions.py b/trade/datamanager/exceptions.py new file mode 100644 index 0000000..f49a703 --- /dev/null +++ b/trade/datamanager/exceptions.py @@ -0,0 +1,7 @@ +class DataManagerException(Exception): + """Base exception for DataManager errors.""" + pass + +class EmptyDataException(DataManagerException): + """Exception raised when data is empty.""" + pass \ No newline at end of file diff --git a/trade/datamanager/forward.py b/trade/datamanager/forward.py new file mode 100644 index 0000000..62e9676 --- /dev/null +++ b/trade/datamanager/forward.py @@ -0,0 +1,1052 @@ +"""Forward price computation and caching for options pricing models. + +This module provides the ForwardDataManager class for computing and caching forward +prices using spot prices, risk-free rates, and dividends. Supports both discrete +(schedule-based) and continuous (yield-based) dividend models with intelligent caching. + +Typical usage: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> result = fwd_mgr.get_forward_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + >>> forwards = result.daily_discrete_forward +""" + +from datetime import datetime, date +from typing import Any, ClassVar, Optional, Tuple, Union +import pandas as pd +import numpy as np +from trade.datamanager.market_data import TimeseriesData +from trade.datamanager.utils.date import is_available_on_date +from trade.helpers.Logging import setup_logger +from trade.helpers.helper import change_to_last_busday, get_missing_dates, to_datetime +from trade.datamanager.utils.data_structure import _data_structure_sanitize +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager.dividend import DividendDataManager +from trade.datamanager.result import ForwardResult, SpotResult +from trade.datamanager.rates import RatesDataManager +from trade.datamanager.config import OptionDataConfig +from trade.datamanager.result import DividendsResult, RatesResult +from trade.datamanager._enums import ArtifactType, Interval, RealTimeFallbackOption, SeriesId +from trade.datamanager.utils.cache import _data_structure_cache_it +from trade.datamanager.utils.logging import get_logging_level +from trade.datamanager.vars import get_times_series, load_name +from trade.optionlib.config.types import DivType +from trade.optionlib.assets.dividend import ( + vectorized_discrete_pv, + SECONDS_IN_DAY, + SECONDS_IN_YEAR, +) +from trade.optionlib.assets.forward import ( + vectorized_forward_discrete, + vectorized_forward_continuous, + get_vectorized_continuous_dividends, +) + +logger = setup_logger("trade.datamanager.forward", stream_log_level=get_logging_level()) +TS = get_times_series() # Load market timeseries data on module import to avoid circular imports + +class ForwardDataManager(BaseDataManager): + """Manages forward price computation and caching for a specific symbol using spot, rates, and dividends. + + Computes forward prices using cost-of-carry models with discrete or continuous dividends. + Implements singleton pattern per symbol to avoid redundant timeseries loading. Supports + both split-adjusted (chain_spot) and unadjusted (spot) price bases. + + Attributes: + CACHE_NAME: Class-level cache identifier for this manager type. + DEFAULT_SERIES_ID: Default historical series identifier. + INSTANCES: Class-level cache of manager instances per symbol. + CONFIG: Configuration object for forward computation settings. + symbol: The equity ticker symbol this manager handles. + + Examples: + >>> # Singleton access - same instance returned for same symbol + >>> fwd_mgr1 = ForwardDataManager("AAPL") + >>> fwd_mgr2 = ForwardDataManager("AAPL") + >>> assert fwd_mgr1 is fwd_mgr2 + + >>> # Get forward price time-series with discrete dividends + >>> result = fwd_mgr1.get_forward_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + >>> forwards = result.daily_discrete_forward + + >>> # Get single forward price for a date + >>> result = fwd_mgr1.get_forward( + ... date="2025-01-15", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE + ... ) + """ + + CACHE_NAME: ClassVar[str] = "forward_data_manager" + DEFAULT_SERIES_ID: ClassVar["SeriesId"] = SeriesId.HIST + INSTANCES: ClassVar[dict[str, "ForwardDataManager"]] = {} + CONFIG: OptionDataConfig = OptionDataConfig() + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + + def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> "ForwardDataManager": + """Returns cached instance for symbol, creating new one if needed. + + Implements singleton pattern per symbol to ensure timeseries are loaded only once. + Automatically loads market timeseries data on first instantiation. + + Args: + symbol: Equity ticker symbol (e.g., "AAPL", "MSFT"). + *args: Additional positional arguments passed to __init__. + **kwargs: Additional keyword arguments passed to __init__. + + Returns: + Singleton ForwardDataManager instance for the given symbol. + + Examples: + >>> mgr1 = ForwardDataManager("AAPL") + >>> mgr2 = ForwardDataManager("AAPL") + >>> assert mgr1 is mgr2 # Same instance + """ + if symbol not in cls.INSTANCES: + instance = super(ForwardDataManager, cls).__new__(cls) + cls.INSTANCES[symbol] = instance + return cls.INSTANCES[symbol] + + def __init__( + self, + symbol: str, + *, + enable_namespacing: bool = False, + ) -> None: + """Initializes manager once per symbol instance. + + Sets up persistent cache for forward price data. Only executes once per + symbol due to singleton pattern. + + Args: + symbol: Equity ticker symbol. + enable_namespacing: If True, enables namespace isolation in cache keys. + + Examples: + >>> mgr = ForwardDataManager("AAPL") + >>> mgr = ForwardDataManager("AAPL", enable_namespacing=True) + """ + if getattr(self, "_initialized", False): + return + + self._initialized = True + super().__init__(enable_namespacing=enable_namespacing, symbol=symbol) + self.symbol = symbol + + def _normalize_inputs( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType], + ) -> Tuple[DivType, date, date, date, str, str, str]: + """Converts date inputs to both date objects and strings. + + Normalizes various date input formats to consistent datetime objects and + YYYY-MM-DD strings. Sets default dividend type to DISCRETE if not specified. + + Args: + start_date: Start date (YYYY-MM-DD string or datetime). + end_date: End date (YYYY-MM-DD string or datetime). + maturity_date: Maturity date (YYYY-MM-DD string or datetime). + dividend_type: Optional DivType. Defaults to DISCRETE if None. + + Returns: + Tuple containing: + - DivType: Dividend type (DISCRETE or CONTINUOUS) + - date: Start date object + - date: End date object + - date: Maturity date object + - str: Start date string (YYYY-MM-DD) + - str: End date string (YYYY-MM-DD) + - str: Maturity date string (YYYY-MM-DD) + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> dividend_type, start_dt, end_dt, mat_dt, start_str, end_str, mat_str = \ + ... fwd_mgr._normalize_inputs( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_type=None + ... ) + >>> logger.info(dividend_type) # DivType.DISCRETE + """ + dividend_type = DivType(dividend_type) if dividend_type is not None else self.CONFIG.dividend_type + + start_dt = datetime.strptime(start_date, "%Y-%m-%d") if isinstance(start_date, str) else start_date + end_dt = datetime.strptime(end_date, "%Y-%m-%d") if isinstance(end_date, str) else end_date + mat_dt = datetime.strptime(maturity_date, "%Y-%m-%d") if isinstance(maturity_date, str) else maturity_date + + start_str = datetime.strftime(start_dt, "%Y-%m-%d") + end_str = datetime.strftime(end_dt, "%Y-%m-%d") + mat_str = datetime.strftime(mat_dt, "%Y-%m-%d") + return dividend_type, start_dt, end_dt, mat_dt, start_str, end_str, mat_str + + def _build_key(self, *, mat_str: str, dividend_type: DivType, use_chain_spot: bool) -> str: + """Constructs cache key from maturity, dividend type, and spot type. + + Creates unique cache identifier incorporating symbol, maturity date, dividend type, + and whether split-adjusted prices are used. + + Args: + mat_str: Maturity date string (YYYY-MM-DD). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + + Returns: + Unique cache key string. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> key = fwd_mgr._build_key( + ... mat_str="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + """ + return self.make_key( + symbol=self.symbol, + artifact_type=ArtifactType.FWD, + series_id=SeriesId.HIST, + maturity=mat_str, + dividend_type=dividend_type.value, + use_chain_spot=use_chain_spot, + interval=Interval.EOD, + ) + + def _try_get_cached( + self, + *, + key: str, + start_str: str, + end_str: str, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + dividend_type: DivType, + use_chain_spot: bool, + ) -> Tuple[Optional[pd.Series], bool, str, str, Optional[ForwardResult]]: + """Checks cache for existing data and identifies missing dates. + + Attempts to retrieve forward prices from cache. If found, checks if the cached + data fully covers the requested date range. Returns cached result directly if + complete, or identifies missing dates that need computation. + + Args: + key: Cache key identifier. + start_str: Start date string (YYYY-MM-DD). + end_str: End date string (YYYY-MM-DD). + start_date: Start date (string or datetime). + end_date: End date (string or datetime). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + Returns: + Tuple containing: + - Optional[pd.Series]: Cached series if partial hit, None otherwise + - bool: True if partial cache hit (need to fetch missing dates) + - str: Start date for fetching (adjusted if partial hit) + - str: End date for fetching (adjusted if partial hit) + - Optional[ForwardResult]: Complete result if full cache hit, None otherwise + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> cached, partial, start, end, result = fwd_mgr._try_get_cached( + ... key="my_key", + ... start_str="2025-01-01", + ... end_str="2025-01-31", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + """ + cached_series = self.get(key, default=None) + if cached_series is None: + return None, False, start_str, end_str, None + + missing = get_missing_dates(cached_series, _start=start_str, _end=end_str) + if not missing: + logger.info(f"Cache hit for forward timeseries key: {key}") + cached_series = _data_structure_sanitize( + cached_series, + start=start_str, + end=end_str, + source_name=f"cached forward timeseries for {self.symbol}", + ) + + result = ForwardResult() + if dividend_type == DivType.DISCRETE: + result.daily_discrete_forward = cached_series + else: + result.daily_continuous_forward = cached_series + result.dividend_type = dividend_type + result.key = key + result.symbol = self.symbol + result.undo_adjust = use_chain_spot + return cached_series, False, start_str, end_str, result + + logger.info( + f"Cache partially covers requested date range for forward timeseries. " + f"Key: {key}. Fetching missing dates: {missing}" + ) + return cached_series, True, min(missing), max(missing), None + + def _get_dividend_result( + self, + *, + start_str: str, + end_str: str, + mat_str: str, + dividend_type: DivType, + dividend_result: Optional[DividendsResult], + use_chain_spot: bool, + ) -> DividendsResult: + """Fetches or validates dividend data with adjustment consistency checks. + + Retrieves dividend data from DividendDataManager if not provided. Validates + that dividend adjustments match the spot price basis (undo_adjust must equal + use_chain_spot for consistency). + + Args: + start_str: Start date string (YYYY-MM-DD). + end_str: End date string (YYYY-MM-DD). + mat_str: Maturity date string (YYYY-MM-DD). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. + dividend_result: Optional pre-computed dividend data. Fetched if None. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + + Returns: + DividendsResult containing dividend schedules or yields. + + Raises: + ValueError: If dividend_result is empty. + ValueError: If dividend_result.undo_adjust != use_chain_spot. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> div_result = fwd_mgr._get_dividend_result( + ... start_str="2025-01-01", + ... end_str="2025-01-31", + ... mat_str="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... dividend_result=None, + ... use_chain_spot=True + ... ) + + Notes: + - If using chain_spot (split-adjusted), dividends must be back-adjusted + - Ensures consistency between spot and dividend adjustment methods + """ + if dividend_result is None: + dividend_result = DividendDataManager(symbol=self.symbol).get_schedule_timeseries( + start_date=start_str, + end_date=end_str, + maturity_date=mat_str, + dividend_type=dividend_type, + undo_adjust=use_chain_spot, # If using chain spot, back adjust dividends + ) + + if dividend_result.is_empty(): + raise ValueError("Dividend result is empty. Cannot compute forward prices without dividend information.") + + if dividend_result.undo_adjust != use_chain_spot: + raise ValueError("Mismatch between dividend_result.undo_adjust and use_chain_spot. They must be the same.") + + return dividend_result + + def _load_spot(self, *, use_chain_spot: bool, spot: Optional[SpotResult] = None) -> pd.Series: + """Loads spot or chain_spot price series. + + Retrieves either split-adjusted (chain_spot) or unadjusted (spot) closing prices + from timeseries data. + + Args: + use_chain_spot: If True, returns split-adjusted chain_spot prices. + spot: Optional pre-loaded TimeseriesData. Fetched from TS if None. + + Returns: + Series of closing prices indexed by datetime. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> spot_prices = fwd_mgr._load_spot(use_chain_spot=True) + >>> logger.info(spot_prices.head()) + datetime + 2025-01-02 155.32 + 2025-01-03 156.01 + ... + + Notes: + - chain_spot: Split-adjusted prices (use with undo_adjust=True dividends) + - spot: Unadjusted prices (use with undo_adjust=False dividends) + """ + if spot is None: + if use_chain_spot: + spot = TS._get_chain_spot_timeseries(sym=self.symbol)["close"] + else: + spot = TS._get_spot_timeseries(sym=self.symbol)["close"] + return spot.timeseries + + def _load_rates(self, *, start_str: str, end_str: str, rates: Optional[RatesResult] = None) -> pd.Series: + """Loads risk-free rates for date range. + + Retrieves risk-free interest rates from RatesDataManager if not provided. + Filters to exact date range requested. + + Args: + start_str: Start date string (YYYY-MM-DD). + end_str: End date string (YYYY-MM-DD). + rates: Optional pre-computed rates data. Fetched if None. + + Returns: + Series of annualized risk-free rates indexed by datetime. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> rates = fwd_mgr._load_rates( + ... start_str="2025-01-01", + ... end_str="2025-01-31" + ... ) + >>> logger.info(rates.head()) + Datetime + 2025-01-02 0.0485 + 2025-01-03 0.0487 + ... + """ + if rates is None: + rates_data = RatesDataManager().get_risk_free_rate_timeseries( + start_date=start_str, + end_date=end_str, + interval=Interval.EOD, + ) + rates = rates_data.daily_risk_free_rates + else: + rates = rates.daily_risk_free_rates + rates = rates[(rates.index >= pd.to_datetime(start_str)) & (rates.index <= pd.to_datetime(end_str))] + return rates + + def _align_3( + self, spot: pd.Series, rates: pd.Series, third: pd.Series, *, third_name: str + ) -> Tuple[pd.Series, pd.Series, pd.Series]: + """Aligns three series to common dates and validates no NaNs. + + Synchronizes spot prices, risk-free rates, and dividend data to a common + date index. Validates that rates and dividend data have no missing values. + + Args: + spot: Series of spot prices. + rates: Series of risk-free rates. + third: Series of dividend data (schedules or yields). + third_name: Descriptive name for third series (for error messages). + + Returns: + Tuple of three aligned Series with common index. + + Raises: + ValueError: If rates contain NaNs after alignment. + ValueError: If third series contains NaNs after alignment. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> spot_aligned, rates_aligned, divs_aligned = fwd_mgr._align_3( + ... spot=spot_series, + ... rates=rates_series, + ... third=dividend_series, + ... third_name="discrete dividend schedules" + ... ) + + Notes: + - Only dates present in all three series are retained + - Spot prices may have NaNs (will be handled by vectorized functions) + - Rates and dividend data must be complete (no NaNs allowed) + """ + idx = spot.index.intersection(rates.index).intersection(third.index) + + spot = spot.reindex(idx) + rates = rates.reindex(idx) + third = third.reindex(idx) + + if rates.isna().any(): + raise ValueError("NaNs in rates after alignment.") + if third.isna().any(): + raise ValueError(f"NaNs in {third_name} after alignment.") + + return spot, rates, third + + def _compute_forward_discrete( + self, + *, + spot: pd.Series, + rates: pd.Series, + discrete_divs: pd.Series, # series of Schedule objects + mat_dt: date, + ) -> pd.Series: + """Computes forward prices using discrete dividend schedules. + + Calculates forward prices using the discrete dividend model: + F = S * exp(r*T) - PV(divs) + + Where PV(divs) is the present value of all dividends between valuation + date and maturity. + + Args: + spot: Series of spot prices. + rates: Series of annualized risk-free rates. + discrete_divs: Series of Schedule objects (dividend events). + mat_dt: Maturity date (e.g., option expiry). + + Returns: + Series of forward prices indexed by valuation dates. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> forwards = fwd_mgr._compute_forward_discrete( + ... spot=spot_prices, + ... rates=risk_free_rates, + ... discrete_divs=dividend_schedules, + ... mat_dt=date(2025, 6, 20) + ... ) + >>> logger.info(forwards.head()) + datetime + 2025-01-02 156.45 + 2025-01-03 157.12 + ... + + Notes: + - Uses vectorized computation for efficiency + - Discounts each dividend in schedule to valuation date + - Time to maturity calculated as (mat_dt - val_dt) in years + """ + pv_divs = vectorized_discrete_pv( + schedules=discrete_divs.to_list(), + r=rates.tolist(), + _valuation_dates=discrete_divs.index.tolist(), + _end_dates=[mat_dt] * len(discrete_divs), + ) + pv_divs = [pv_divs] if isinstance(pv_divs, (int, float)) else pv_divs + + second_vector = [(mat_dt - val).days * SECONDS_IN_DAY for val in discrete_divs.index] + t = [val / SECONDS_IN_YEAR for val in second_vector] + + forwards = vectorized_forward_discrete( + S=spot.tolist(), + r=rates.tolist(), + T=t, + pv_divs=pv_divs, + ) + return pd.Series(data=forwards, index=discrete_divs.index) + + def _compute_forward_continuous( + self, + *, + spot: pd.Series, + rates: pd.Series, + continuous_divs: pd.Series, # series of dividend yields + mat_dt: date, + ) -> pd.Series: + """Computes forward prices using continuous dividend yields. + + Calculates forward prices using the continuous dividend model: + F = S * exp((r - q) * T) + + Where q is the continuous dividend yield. + + Args: + spot: Series of spot prices. + rates: Series of annualized risk-free rates. + continuous_divs: Series of annualized dividend yields. + mat_dt: Maturity date (e.g., option expiry). + + Returns: + Series of forward prices indexed by valuation dates. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> forwards = fwd_mgr._compute_forward_continuous( + ... spot=spot_prices, + ... rates=risk_free_rates, + ... continuous_divs=dividend_yields, + ... mat_dt=date(2025, 6, 20) + ... ) + >>> logger.info(forwards.head()) + datetime + 2025-01-02 156.28 + 2025-01-03 156.95 + ... + + Notes: + - Uses vectorized computation for efficiency + - Assumes constant dividend yield between valuation and maturity + - Time to maturity calculated as (mat_dt - val_dt) in years + """ + q_factor = get_vectorized_continuous_dividends( + div_rates=continuous_divs.tolist(), + _valuation_dates=continuous_divs.index.tolist(), + _end_dates=[mat_dt] * len(continuous_divs), + ) + + second_vector = [(mat_dt - val).days * SECONDS_IN_DAY for val in continuous_divs.index] + t = [val / SECONDS_IN_YEAR for val in second_vector] + + forwards = vectorized_forward_continuous( + S=spot.tolist(), + r=rates.tolist(), + T=t, + q_factor=q_factor, + ) + return pd.Series(data=forwards, index=continuous_divs.index) + + def _merge_partial(self, cached_series: pd.Series, forward_series: pd.Series) -> pd.Series: + """Merges newly computed data with cached data, keeping latest values. + + Combines partial cache hits with newly computed forward prices, deduplicating + by index and keeping the most recent values. + + Args: + cached_series: Existing cached forward prices. + forward_series: Newly computed forward prices. + + Returns: + Merged Series with deduplicated index. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> merged = fwd_mgr._merge_partial( + ... cached_series=old_forwards, + ... forward_series=new_forwards + ... ) + + Notes: + - Duplicates are removed, keeping 'last' (newest) values + - Useful when cache partially covers requested date range + """ + merged = pd.concat([cached_series, forward_series]) + forward_series = merged[~merged.index.duplicated(keep="last")] + return forward_series + + def get_forward_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + spot: Optional[TimeseriesData] = None, + rates: Optional[RatesResult] = None, + *, + dividend_result: Optional[DividendsResult] = None, + use_chain_spot: bool = True, + ) -> ForwardResult: + """Returns daily forward price time-series from valuation dates to maturity. + + Computes forward prices for each business day in [start_date, end_date], + where each forward is valued to the fixed maturity_date. Uses discrete + dividends (Schedule objects) or continuous yields depending on dividend_type. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + maturity_date: Fixed horizon date for all forwards (e.g., option expiry). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to DISCRETE. + spot: Optional pre-loaded TimeseriesData. Fetched if None. + rates: Optional pre-computed rates data. Fetched if None. + dividend_result: Pre-computed dividend data. If None, fetches internally. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + + Returns: + ForwardResult containing daily_discrete_forward or daily_continuous_forward + Series with DatetimeIndex, plus the dividend_result used and cache key. + + Raises: + ValueError: If maturity_date < start_date. + ValueError: If dividend_result.undo_adjust != use_chain_spot. + + Examples: + >>> # Basic usage with automatic data fetching + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> result = fwd_mgr.get_forward_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + >>> logger.info(result.daily_discrete_forward.head()) + datetime + 2025-01-02 155.32 + 2025-01-03 156.01 + ... + + >>> # Provide pre-computed data for efficiency + >>> div_mgr = DividendDataManager("AAPL") + >>> div_result = div_mgr.get_schedule_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... undo_adjust=True + ... ) + >>> fwd_result = fwd_mgr.get_forward_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... maturity_date="2025-06-20", + ... dividend_result=div_result, + ... use_chain_spot=True + ... ) + + Notes: + - Partial cache hits only compute missing dates + - Cache expires after 12 hours + - Spot, rates, and dividends are aligned to common dates + - start_date/end_date define valuation date range + - maturity_date is the fixed horizon (e.g., option expiry) + """ + + ## Load first + load_name(self.symbol) + + ## Normalize inputs + result = ForwardResult() + og_start_date = start_date + og_end_date = end_date + dividend_type, start_dt, end_dt, mat_dt, start_str, end_str, mat_str = self._normalize_inputs( + start_date=start_date, + end_date=end_date, + maturity_date=maturity_date, + dividend_type=dividend_type, + ) + + if mat_dt < start_dt: + raise ValueError("maturity_date must be >= start_date") + + key = self._build_key(mat_str=mat_str, dividend_type=dividend_type, use_chain_spot=use_chain_spot) + + cached_series, partial_hit, start_str, end_str, cached_result = self._try_get_cached( + key=key, + start_str=start_str, + end_str=end_str, + start_date=start_date, + end_date=end_date, + dividend_type=dividend_type, + use_chain_spot=use_chain_spot, + ) + if cached_result is not None: + return cached_result + + dividend_result = self._get_dividend_result( + start_str=start_str, + end_str=end_str, + mat_str=mat_str, + dividend_type=dividend_type, + dividend_result=dividend_result, + use_chain_spot=use_chain_spot, + ) + spot = self._load_spot(use_chain_spot=use_chain_spot, spot=spot) + rates = self._load_rates(start_str=start_str, end_str=end_str, rates=rates) + + if dividend_type == DivType.DISCRETE: + discrete_divs = dividend_result.daily_discrete_dividends + + spot, rates, discrete_divs = self._align_3( + spot=spot, + rates=rates, + third=discrete_divs, + third_name="discrete dividend schedules", + ) + + forward_series = self._compute_forward_discrete( + spot=spot, + rates=rates, + discrete_divs=discrete_divs, + mat_dt=mat_dt, + ) + + result.daily_discrete_forward = forward_series + result.dividend_result = dividend_result + + elif dividend_type == DivType.CONTINUOUS: + continuous_divs = dividend_result.daily_continuous_dividends + + spot, rates, continuous_divs = self._align_3( + spot=spot, + rates=rates, + third=continuous_divs, + third_name="div yields", + ) + + forward_series = self._compute_forward_continuous( + spot=spot, + rates=rates, + continuous_divs=continuous_divs, + mat_dt=mat_dt, + ) + + result.daily_continuous_forward = forward_series + result.dividend_result = dividend_result + result.symbol = self.symbol + result.undo_adjust = use_chain_spot + + else: + raise ValueError(f"Unsupported dividend type: {dividend_type}") + + result.dividend_type = dividend_type + result.key = key + + if partial_hit: + forward_series = self._merge_partial(cached_series=cached_series, forward_series=forward_series) + + self.cache_it(key, forward_series, expire=86400) # 24 hours expiry + forward_series = _data_structure_sanitize( + forward_series, + start=og_start_date, + end=og_end_date, + source_name=f"forward timeseries for {self.symbol} with maturity {mat_str}", + ) + + if dividend_type == DivType.DISCRETE: + result.daily_discrete_forward = forward_series + else: + result.daily_continuous_forward = forward_series + + result.undo_adjust = use_chain_spot + result.symbol = self.symbol + result.undo_adjust = use_chain_spot + + return result + + def make_key(self, *, symbol: str, interval=None, artifact_type=None, series_id=None, **extra_parts) -> str: + """Delegates to BaseDataManager key construction. + + Constructs cache key by forwarding to parent class method. + + Args: + symbol: Ticker symbol. + interval: Time interval (e.g., Interval.EOD). + artifact_type: Type of artifact (e.g., ArtifactType.FWD). + series_id: Series identifier (e.g., SeriesId.HIST). + **extra_parts: Additional key components (maturity, dividend_type, etc.). + + Returns: + Unique cache key string. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> key = fwd_mgr.make_key( + ... symbol="AAPL", + ... artifact_type=ArtifactType.FWD, + ... maturity="2025-06-20" + ... ) + """ + return super().make_key( + symbol=symbol, interval=interval, artifact_type=artifact_type, series_id=series_id, **extra_parts + ) + + def cache_it(self, key: str, value: pd.Series, *, expire: Optional[int] = None) -> None: + """Merges and caches forward time-series, excluding today's partial data. + + Appends new forward price data to existing cached time-series if cache entry exists. + Filters out today's data to avoid caching incomplete/changing values. + + Args: + key: Cache key identifier. + value: Series of forward prices to cache (indexed by datetime). + expire: Optional expiration time in seconds. Uses cache default if None. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> forwards = pd.Series([156.45, 157.12], index=pd.date_range("2025-01-01", periods=2)) + >>> fwd_mgr.cache_it("my_key", forwards, expire=43200) # 12 hours + + Notes: + - Existing cache entries are merged with new data + - Duplicates are removed, keeping latest values + - Today's data excluded to avoid caching incomplete values + """ + ## Since it is a timeseries, we will append to existing if exists + _data_structure_cache_it(self, key, value, expire=expire) + return + + def get_forward( + self, + date: Union[datetime, str], + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + dividend_result: Optional[DividendsResult] = None, + spot: Optional[TimeseriesData] = None, + rates: Optional[RatesResult] = None, + *, + use_chain_spot: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + ) -> ForwardResult: + """Returns the forward price at a specific valuation date. + + Computes forward price for a single valuation date to maturity. Wrapper around + get_forward_timeseries with single-date range. + + Args: + date: Valuation date (YYYY-MM-DD string or datetime). + maturity_date: Horizon date (e.g., option expiry). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to DISCRETE. + dividend_result: Optional pre-computed dividend data. + spot: Optional pre-loaded TimeseriesData. + rates: Optional pre-computed rates data. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + fallback_option: Optional fallback option for real-time data. + Returns: + ForwardResult containing single forward price in daily_discrete_forward + or daily_continuous_forward Series. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> result = fwd_mgr.get_forward( + ... date="2025-01-15", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + >>> forward_price = result.daily_discrete_forward.iloc[0] + >>> logger.info(f"Forward: ${forward_price:.2f}") + Forward: $156.45 + + Notes: + - Suitable for real-time pricing scenarios + - Internally calls get_forward_timeseries with date as both start and end + """ + load_name(self.symbol) + dividend_type = DivType(dividend_type) if dividend_type is not None else DivType.DISCRETE + fallback_option = fallback_option if fallback_option is not None else self.CONFIG.real_time_fallback_option + date = to_datetime(date) + + + if not is_available_on_date(date): + logger.warning( + f"Valuation date {date} is not a business day or holiday. No dividends available. Resolution: {fallback_option}" + ) + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"Valuation date {date} is not a business day or holiday.") + if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## Using only change_to_last_busday assumes input date is not business day or is holiday + ## Which the function would roll back + ## But there's a possibility input date is today's date but before market open + ## In that case we need to move back one more business day + date = change_to_last_busday(date - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + else: + result = ForwardResult() + if dividend_type == DivType.DISCRETE: + result.daily_discrete_forward = pd.Series( + dtype=float, + index=[date], + data=[np.nan if fallback_option == RealTimeFallbackOption.NAN else 0.0], + ) + else: + result.daily_continuous_forward = pd.Series( + dtype=float, + index=[date], + data=[np.nan if fallback_option == RealTimeFallbackOption.NAN else 0.0], + ) + + result.key = None + result.undo_adjust = use_chain_spot + result.dividend_type = dividend_type + result.symbol = self.symbol + result.fallback_option = fallback_option + return result + + date_str = date.strftime("%Y-%m-%d") if isinstance(date, datetime) else date + mat_str = maturity_date.strftime("%Y-%m-%d") if isinstance(maturity_date, datetime) else maturity_date + start = date_str + end = date_str + + + result = self.get_forward_timeseries( + start_date=start, + end_date=end, + maturity_date=mat_str, + dividend_type=dividend_type, + use_chain_spot=use_chain_spot, + dividend_result=dividend_result, + spot=spot, + rates=rates, + ) + result.fallback_option = fallback_option + return result + + def rt( + self, + maturity_date: Union[datetime, str], + dividend_type: Optional[DivType] = None, + dividend_result: Optional[DividendsResult] = None, + spot: Optional[TimeseriesData] = None, + rates: Optional[RatesResult] = None, + *, + use_chain_spot: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + ) -> ForwardResult: + """Shortcut for get_forward method. + + Provides a concise alias for retrieving forward prices at the current date. + + Args: + maturity_date: Horizon date (e.g., option expiry). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to DISCRETE. + dividend_result: Optional pre-computed dividend data. + spot: Optional pre-loaded TimeseriesData. + rates: Optional pre-computed rates data. + use_chain_spot: If True, uses split-adjusted chain_spot prices. + fallback_option: Optional fallback option for real-time data. + + Returns: + ForwardResult containing single forward price in daily_discrete_forward + or daily_continuous_forward Series. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> result = fwd_mgr.rt( + ... date="2025-01-15", + ... maturity_date="2025-06-20", + ... dividend_type=DivType.DISCRETE, + ... use_chain_spot=True + ... ) + >>> forward_price = result.daily_discrete_forward.iloc[0] + >>> logger.info(f"Forward: ${forward_price:.2f}") + Forward: $156.45 + """ + load_name(self.symbol) + res = self.get_forward( + date=datetime.now(), + maturity_date=maturity_date, + dividend_type=dividend_type, + dividend_result=dividend_result, + spot=spot, + rates=rates, + use_chain_spot=use_chain_spot, + fallback_option=fallback_option, + ) + res.rt = True + return res + + def offload(self, *args: Any, **kwargs: Any) -> None: + """Placeholder for offload logic (not implemented). + + Reserved for future implementation of cache offloading or cleanup operations. + Currently performs no action. + + Args: + *args: Arbitrary positional arguments. + **kwargs: Arbitrary keyword arguments. + + Examples: + >>> fwd_mgr = ForwardDataManager("AAPL") + >>> fwd_mgr.offload() # No-op + """ + logger.info(f"No offload logic implemented for {self.CACHE_NAME}") diff --git a/trade/datamanager/greeks.py b/trade/datamanager/greeks.py new file mode 100644 index 0000000..f167328 --- /dev/null +++ b/trade/datamanager/greeks.py @@ -0,0 +1,954 @@ +"""Greek data manager for computing option sensitivities (delta, gamma, vega, theta, rho). + +This module provides the GreekDataManager class for calculating option greeks using +various pricing models (Black-Scholes-Merton, Cox-Ross-Rubinstein binomial). It handles +the complete workflow including data loading, caching, model selection, and result +formatting. + +Key Features: + - Multiple pricing models: BSM, CRR binomial + - Support for American and European exercise styles + - Discrete and continuous dividend treatments + - Automatic data loading and caching + - Real-time and historical greek calculation + - Configurable greek selection (compute only needed greeks) + +Typical Usage: + >>> from trade.datamanager.greeks import GreekDataManager + >>> from trade.datamanager._enums import GreekType + >>> from trade.optionlib.config.types import DivType + >>> + >>> # Initialize manager for AAPL + >>> greek_mgr = GreekDataManager("AAPL") + >>> + >>> # Get all greeks for an option + >>> result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE, + ... ) + >>> print(result.timeseries[["delta", "gamma", "vega"]].head()) + >>> + >>> # Get only specific greeks + >>> result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA] + ... ) +""" + +from datetime import datetime +from typing import Optional, Union, List +import pandas as pd +from trade.datamanager.result import ( + GreekResultSet, + SpotResult, + RatesResult, + DividendsResult, + VolatilityResult, + ForwardResult, +) +from trade.datamanager.utils.vol_helpers import ( + _handle_cache_for_vol, + _merge_and_cache_vol_result, + _prepare_vol_calculation_setup, +) +from trade.datamanager.utils.date import sync_date_index, is_available_on_date +from trade.datamanager.utils.model import _load_model_data_timeseries, LoadRequest +from trade.datamanager.utils.greeks_helpers import _prepare_greeks_to_compute, _get_prefilled_greek_result_set +from trade.datamanager._enums import ( + GreekType, + ModelPrice, + OptionPricingModel, + OptionSpotEndpointSource, + VolatilityModel, + RealTimeFallbackOption, + ArtifactType, + Interval, +) +from trade.optionlib.greeks.numerical.binomial import binomial_tree_greeks +from trade.optionlib.greeks.numerical.black_scholes import vectorized_black_scholes_greeks +from trade.optionlib.assets.dividend import ( + vectorized_discrete_pv, + get_vectorized_continuous_dividends, + vector_convert_to_time_frac, +) +from trade.helpers.helper import to_datetime, change_to_last_busday +from trade.datamanager._enums import SeriesId +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager.config import OptionDataConfig +from trade.helpers.helper_types import DATE_HINT +from trade.optionlib.config.types import DivType +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME + +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + + +class GreekDataManager(BaseDataManager): + """Manager for computing and caching option greeks (delta, gamma, vega, theta, rho). + + Class that orchestrates the computation of option sensitivities (greeks) using + various option pricing models. Automatically loads required market data (spot, + forward, rates, dividends, implied volatilities) and caches results for efficient reuse. + + Supports two pricing approaches: + 1. Black-Scholes-Merton (BSM) - Fast, European-style greeks + 2. Cox-Ross-Rubinstein (CRR) - Binomial tree, supports American exercise + + Attributes: + CONFIG: Configuration object with default settings for pricing models. + CACHE_NAME: Cache identifier for greek data. + CACHE_SPEC: Cache specification for data persistence. + DEFAULT_SERIES_ID: Default series identifier (historical data). + symbol: Ticker symbol for the underlying asset. + + Examples: + >>> # Basic usage with BSM model + >>> greek_mgr = GreekDataManager("AAPL") + >>> result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + + >>> # Get only delta and gamma + >>> from trade.datamanager._enums import GreekType + >>> result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA] + ... ) + + >>> # Real-time greeks + >>> rt_greeks = greek_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + """ + + CONFIG: OptionDataConfig = OptionDataConfig() + CACHE_NAME: str = "greek_datamanager_cache" + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + DEFAULT_SERIES_ID: SeriesId = SeriesId.HIST + + def __init__(self, symbol: str): + """Initialize GreekDataManager with symbol-specific configuration. + + Args: + symbol: Ticker symbol for the underlying asset (e.g., "AAPL", "MSFT"). + + Examples: + >>> greek_mgr = GreekDataManager("AAPL") + """ + super().__init__(symbol=symbol) + + def get_greeks_timeseries( + self, + start_date: DATE_HINT, + end_date: DATE_HINT, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: Optional[DivType] = None, + *, + greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS, + f: Optional[ForwardResult] = None, + S: Optional[SpotResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + market_model: Optional[OptionPricingModel] = None, + model_price: Optional[ModelPrice] = None, + undo_adjust: bool = True, + ) -> GreekResultSet: + """Returns daily option greeks timeseries using specified pricing model. + + Computes option sensitivities (delta, gamma, vega, theta, rho) for each business day + in [start_date, end_date]. Automatically selects appropriate pricing model (BSM or + binomial) and loads required market data. Uses caching to avoid redundant computations. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to CONFIG setting. + greeks_to_compute: Which greeks to compute. Single GreekType or list of GreekTypes. + Defaults to GreekType.GREEKS (all standard greeks). Available: DELTA, GAMMA, + VEGA, THETA, RHO, CHARM, VANNA, SPEED, ZOMMA, COLOR, ULTIMA. + f: Optional pre-computed forward prices. If None, loads automatically. + S: Optional pre-computed spot prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + d: Optional pre-computed dividend data. If None, loads automatically. + vol: Optional pre-computed implied volatilities. If None, loads automatically. + endpoint_source: Option data source (ORATS, HIST, QUOTE). Defaults to CONFIG setting. + market_model: OptionPricingModel.BSM or BINOMIAL. Defaults to CONFIG setting. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). Defaults to CONFIG setting. + undo_adjust: If True, uses split-adjusted prices. + + Returns: + GreekResultSet containing DataFrame with computed greeks as columns and + DatetimeIndex, plus model metadata and cache key. + + Raises: + ValueError: If unsupported market model is specified. + + Examples: + >>> # Basic usage - compute all greeks + >>> greek_mgr = GreekDataManager("AAPL") + >>> result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE + ... ) + >>> print(result.timeseries[["delta", "gamma", "vega"]].head()) + + >>> # Compute only delta and gamma with binomial model + >>> from trade.datamanager._enums import GreekType, OptionPricingModel + >>> result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="p", + ... greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA], + ... market_model=OptionPricingModel.BINOMIAL + ... ) + + >>> # Provide pre-computed volatility data + >>> vol_mgr = VolDataManager("AAPL") + >>> vol_result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + >>> greek_result = greek_mgr.get_greeks_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... vol=vol_result + ... ) + """ + dividend_type = dividend_type or self.CONFIG.dividend_type + endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + market_model = market_model or self.CONFIG.option_model + vol_model = VolatilityModel.MARKET + + result = _get_prefilled_greek_result_set( + key=None, + symbol=self.symbol, + strike=strike, + expiration=expiration, + right=right, + endpoint_source=endpoint_source, + market_model=market_model, + vol_model=vol_model, + dividend_type=dividend_type, + model_price=model_price, + undo_adjust=undo_adjust, + ) + if market_model == OptionPricingModel.BINOMIAL: + return self._get_binomial_greeks( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + result=result, + greeks_to_compute=greeks_to_compute, + S=S, + r=r, + d=d, + vol=vol, + endpoint_source=endpoint_source, + undo_adjust=undo_adjust, + model_price=model_price, + ) + elif market_model == OptionPricingModel.BSM or market_model == OptionPricingModel.EURO_EQIV: + return self._get_bsm_greeks( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + result=result, + greeks_to_compute=greeks_to_compute, + f=f, + S=S, + r=r, + d=d, + vol=vol, + endpoint_source=endpoint_source, + undo_adjust=undo_adjust, + model_price=model_price, + ) + else: + raise ValueError(f"Unsupported market model: {market_model}") + + def _get_binomial_greeks( + self, + start_date: DATE_HINT, + end_date: DATE_HINT, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: Optional[DivType] = None, + *, + result: Optional[GreekResultSet] = None, + greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = None, + S: Optional[SpotResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + model_price: Optional[ModelPrice] = None, + undo_adjust: bool = True, + ) -> GreekResultSet: + """Compute option greeks using Cox-Ross-Rubinstein binomial tree model. + + Internal method that calculates daily option sensitivities using CRR binomial trees. + Supports American exercise. Automatically loads required data (spot, rates, dividends, + implied volatilities) if not provided. Uses caching for efficient reuse. + + Note: Binomial tree model computes all greeks simultaneously, so caching stores the + complete set even if only specific greeks are requested. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + result: Optional pre-initialized GreekResultSet container. + greeks_to_compute: Which greeks to return (all are computed regardless). + S: Optional pre-computed spot prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + d: Optional pre-computed dividend data. If None, loads automatically. + vol: Optional pre-computed implied volatilities. If None, loads automatically. + endpoint_source: Option data source for volatility calculation. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). + undo_adjust: If True, uses split-adjusted prices. + + Returns: + GreekResultSet containing DataFrame with computed greeks as columns and + DatetimeIndex, plus model metadata and cache key. + + Examples: + >>> # Internal usage - typically called via get_greeks_timeseries + >>> result = greek_mgr._get_binomial_greeks( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE + ... ) + """ + + ## biomial tree greeks calculation function calculates all greeks at once. So I'll check cache + ## for a greek and if missing, compute all and store in cache. + ## endpoint_source & div_type will resolved at `get_timeseries` level; the frontend function. + + endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + model_price = model_price or self.CONFIG.model_price + result = result or GreekResultSet() + result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = ( + _prepare_vol_calculation_setup( + self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result + ) + ) + ## Using self.CONFIG allows frontend to override default settings for greeks_to_compute. + ## Also allows user to specify greeks_to_compute at function call level which can get hidden as calls become nested. + greeks_to_compute = greeks_to_compute or self.CONFIG.greeks_to_compute + greeks_to_compute = _prepare_greeks_to_compute(greeks_to_compute) + key = self.make_key( + symbol=self.symbol, + interval=Interval.EOD, + artifact_type=ArtifactType.GREEKS, + series_id=SeriesId.HIST, + option_pricing_model=OptionPricingModel.BINOMIAL, + volatility_model=VolatilityModel.MARKET, + model_price=model_price, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + expiration=expiration, + strike=strike, + right=right, + ) + result.key = key + result.model_price = model_price + + cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol( + self, key, start_date, end_date, result, optional_name="greeks" + ) + if early_return: + result.timeseries = cached_data[greeks_to_compute] + return result + + request = self._create_load_request( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + market_model=OptionPricingModel.BINOMIAL, + model_price=model_price, + endpoint_source=endpoint_source, + s=S, + r=r, + d=d, + vol=vol, + undo_adjust=undo_adjust, + ) + model_data = _load_model_data_timeseries(request) + S = model_data.spot.timeseries if request.load_spot else S.timeseries + r = model_data.rates.timeseries if request.load_rates else r.timeseries + d = model_data.dividend.timeseries if request.load_dividend else d.timeseries + vol = model_data.vol.timeseries if request.load_vol else vol.timeseries + S, r, d, vol = sync_date_index(S, r, d, vol) + + if dividend_type == DivType.DISCRETE: + d = vector_convert_to_time_frac( + schedules=d, + valuation_dates=to_datetime(S.index.tolist(), format="%Y-%m-%d"), + end_dates=to_datetime([expiration] * len(S), format="%Y-%m-%d"), + ) + + ## Now compute greeks + greeks_res_dict = binomial_tree_greeks( + K=[strike] * len(S), + expiration=[expiration] * len(S), + sigma=vol, + S=S, + r=r, + N=[100] * len(S), + dividend_type=[dividend_type.value] * len(S), + div_amount=d, + option_type=[right] * len(S), + start_date=to_datetime(S.index.tolist(), format="%Y-%m-%d"), + valuation_date=to_datetime(S.index.tolist(), format="%Y-%m-%d"), + american=[True] * len(S), + ) + + ## Remove "models" key if exists + if "models" in greeks_res_dict: + del greeks_res_dict["models"] + + greeks_df = pd.DataFrame(greeks_res_dict, index=S.index) + + ## Use utility: Merge and cache + greeks_df = _merge_and_cache_vol_result(self, greeks_df, cached_data, is_partial, key, start_str, end_str) + result.timeseries = greeks_df[greeks_to_compute] + + return result + + def _get_bsm_greeks( + self, + start_date: DATE_HINT, + end_date: DATE_HINT, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: Optional[DivType] = None, + *, + result: Optional[GreekResultSet] = None, + greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS, + f: Optional[ForwardResult] = None, + S: Optional[SpotResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + model_price: Optional[ModelPrice] = None, + undo_adjust: bool = True, + ) -> GreekResultSet: + """Compute option greeks using Black-Scholes-Merton model. + + Internal method that calculates daily option sensitivities using closed-form BSM + formulas. Only supports European-style greeks. Automatically loads required data + (forward, spot, rates, dividends, implied volatilities) if not provided. Uses + caching for efficient reuse. + + Note: BSM model computes all greeks simultaneously, so caching stores the complete + set even if only specific greeks are requested. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + result: Optional pre-initialized GreekResultSet container. + greeks_to_compute: Which greeks to return (all are computed regardless). + f: Optional pre-computed forward prices. If None, loads automatically. + S: Optional pre-computed spot prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + d: Optional pre-computed dividend data. If None, loads automatically. + vol: Optional pre-computed implied volatilities. If None, loads automatically. + endpoint_source: Option data source for volatility calculation. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). + undo_adjust: If True, uses split-adjusted prices. + + Returns: + GreekResultSet containing DataFrame with computed greeks as columns and + DatetimeIndex, plus model metadata and cache key. + + Examples: + >>> # Internal usage - typically called via get_greeks_timeseries + >>> result = greek_mgr._get_bsm_greeks( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE + ... ) + """ + + ## biomial tree greeks calculation function calculates all greeks at once. So I'll check cache + ## for a greek and if missing, compute all and store in cache. + ## endpoint_source & div_type will resolved at `get_timeseries` level; the frontend function. + endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + model_price = model_price or self.CONFIG.model_price + result = result or GreekResultSet() + result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = ( + _prepare_vol_calculation_setup( + self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result + ) + ) + + greeks_to_compute = _prepare_greeks_to_compute(greeks_to_compute) + key = self.make_key( + symbol=self.symbol, + interval=Interval.EOD, + artifact_type=ArtifactType.GREEKS, + series_id=SeriesId.HIST, + option_pricing_model=OptionPricingModel.BSM, + volatility_model=VolatilityModel.MARKET, + model_price=model_price, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + expiration=expiration, + strike=strike, + right=right, + ) + result.key = key + result.model_price = model_price + result.endpoint_source = endpoint_source + + cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol( + self, key, start_date, end_date, result, optional_name="greeks" + ) + if early_return: + result.timeseries = cached_data[greeks_to_compute] + return result + + request = self._create_load_request( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + market_model=OptionPricingModel.BSM, + endpoint_source=endpoint_source, + s=S, + f=f, + r=r, + d=d, + vol=vol, + undo_adjust=undo_adjust, + model_price=model_price, + ) + model_data = _load_model_data_timeseries(request) + S = model_data.spot.timeseries if request.load_spot else S.timeseries + r = model_data.rates.timeseries if request.load_rates else r.timeseries + d = model_data.dividend.timeseries if request.load_dividend else d.timeseries + vol = model_data.vol.timeseries if request.load_vol else vol.timeseries + f = model_data.forward.timeseries if request.load_forward else f.timeseries + s, f, r, d, vol = sync_date_index(S, f, r, d, vol) + + ## Convert dividends to present value amounts + if dividend_type == DivType.DISCRETE: + pv_divs = vectorized_discrete_pv( + schedules=d, + _valuation_dates=f.index.tolist(), + _end_dates=[expiration] * len(f), + r=r, + ) + + ## Continuous dividends. Discount dividend rates to present value amounts + else: + pv_divs = get_vectorized_continuous_dividends( + div_rates=d.values, _valuation_dates=f.index.tolist(), _end_dates=[expiration] * len(f) + ) + + ## Now compute greeks + greeks_res_dict = vectorized_black_scholes_greeks( + S=s, + K=[strike] * len(s), + F=f, + r=r, + sigma=vol, + valuation_dates=s.index.tolist(), + end_dates=[expiration] * len(s), + option_type=[right.lower()] * len(s), + dividend_type=dividend_type.value, + div_amount=pv_divs, + ) + ## Remove "models" key if exists + if "models" in greeks_res_dict: + del greeks_res_dict["models"] + + greeks_df = pd.DataFrame(greeks_res_dict, index=s.index) + + ## Use utility: Merge and cache + greeks_df = _merge_and_cache_vol_result(self, greeks_df, cached_data, is_partial, key, start_str, end_str) + result.timeseries = greeks_df[greeks_to_compute] + + return result + + def get_at_time_greeks( + self, + as_of: DATE_HINT, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: Optional[DivType] = None, + *, + greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS, + S: Optional[SpotResult] = None, + f: Optional[ForwardResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + market_model: Optional[OptionPricingModel] = None, + undo_adjust: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + model_price: Optional[ModelPrice] = None, + ) -> GreekResultSet: + """Get option greeks at a specific point in time. + + Computes option sensitivities for a single valuation date. Handles non-business days + and holidays according to fallback_option setting. Useful for historical analysis on + specific dates or intraday calculations. + + Args: + as_of: Valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to CONFIG setting. + greeks_to_compute: Which greeks to compute. Single GreekType or list of GreekTypes. + S: Optional pre-computed spot prices. If None, loads automatically. + f: Optional pre-computed forward prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + d: Optional pre-computed dividend data. If None, loads automatically. + vol: Optional pre-computed implied volatilities. If None, loads automatically. + endpoint_source: Option data source (ORATS, HIST, QUOTE). Defaults to CONFIG setting. + market_model: OptionPricingModel.BSM or BINOMIAL. Defaults to CONFIG setting. + undo_adjust: If True, uses split-adjusted prices. + fallback_option: How to handle non-business days (RAISE_ERROR, USE_LAST_AVAILABLE, + NAN, ZERO). Defaults to CONFIG setting. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). Defaults to CONFIG setting. + + Returns: + GreekResultSet containing single-row DataFrame with computed greeks as columns, + plus model metadata and cache key. + + Raises: + ValueError: If as_of is not a business day and fallback_option is RAISE_ERROR. + + Examples: + >>> # Get greeks for a specific date + >>> greek_mgr = GreekDataManager("AAPL") + >>> result = greek_mgr.get_at_time_greeks( + ... as_of="2025-01-15", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + >>> print(result.timeseries[["delta", "gamma"]].iloc[0]) + + >>> # Handle non-business day with last available data + >>> from trade.datamanager._enums import RealTimeFallbackOption + >>> result = greek_mgr.get_at_time_greeks( + ... as_of="2025-01-18", # Saturday + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... fallback_option=RealTimeFallbackOption.USE_LAST_AVAILABLE + ... ) + """ + + vol_model = VolatilityModel.MARKET + dividend_type = dividend_type or self.CONFIG.dividend_type + endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + market_model = market_model or self.CONFIG.option_model + fallback_option = fallback_option or self.CONFIG.real_time_fallback_option + model_price = model_price or self.CONFIG.model_price + if not is_available_on_date(as_of): + logger.warning( + f"Valuation date {as_of} is not a business day or holiday. Resolving using fallback options {fallback_option}." + ) + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"Valuation date {as_of} is not a business day or holiday.") + if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## Using only change_to_last_busday assumes input date is not business day or is holiday + ## Which the function would roll back + ## But there's a possibility input date is today's date but before market open + ## In that case we need to move back one more business day + logger.info("Using last available business day for valuation date.") + as_of = change_to_last_busday((as_of - pd.tseries.offsets.BDay(1)), time_of_day_aware=False) + logger.info(f"New valuation date: {as_of}") + else: + result = GreekResultSet() + v = float("nan") if fallback_option == RealTimeFallbackOption.NAN else 0.0 + value_dict = {g: [v] for g in _prepare_greeks_to_compute(greeks_to_compute)} + result.timeseries = pd.DataFrame(data=value_dict, index=pd.DatetimeIndex([to_datetime(as_of)])) + result.key = None + result.vol_model = vol_model or self.CONFIG.volatility_model + result.market_model = market_model or self.CONFIG.option_model + result.expiration = to_datetime(expiration) + result.right = right + result.strike = strike + result.endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + result.dividend_type = dividend_type or self.CONFIG.dividend_type + result.symbol = self.symbol + result.model_price = model_price + result.fallback_option = fallback_option + return result + + greeks_result_set = self.get_greeks_timeseries( + start_date=as_of, + end_date=as_of, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + greeks_to_compute=greeks_to_compute, + S=S, + r=r, + d=d, + vol=vol, + f=f, + endpoint_source=endpoint_source, + market_model=market_model, + undo_adjust=undo_adjust, + model_price=model_price, + ) + greeks_result_set.fallback_option = fallback_option + return greeks_result_set + + def rt( + self, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: Optional[DivType] = None, + *, + greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS, + S: Optional[SpotResult] = None, + f: Optional[ForwardResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + market_model: Optional[OptionPricingModel] = None, + undo_adjust: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + model_price: Optional[ModelPrice] = None, + ) -> GreekResultSet: + """Get real-time option greeks using current market data. + + Convenience method that computes greeks as of current datetime using QUOTE endpoint + for live market prices. Ideal for real-time trading systems and live option monitoring. + + Args: + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to CONFIG setting. + greeks_to_compute: Which greeks to compute. Single GreekType or list of GreekTypes. + S: Optional pre-computed spot prices. If None, loads real-time data. + f: Optional pre-computed forward prices. If None, loads real-time data. + r: Optional pre-computed risk-free rates. If None, loads real-time data. + d: Optional pre-computed dividend data. If None, loads real-time data. + vol: Optional pre-computed implied volatilities. If None, loads real-time data. + market_model: OptionPricingModel.BSM or BINOMIAL. Defaults to CONFIG setting. + undo_adjust: If True, uses split-adjusted prices. + fallback_option: How to handle market closed (USE_LAST_AVAILABLE, NAN, ZERO). + Defaults to CONFIG setting. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). Defaults to CONFIG setting. + + Returns: + GreekResultSet containing single-row DataFrame with computed greeks as columns, + plus model metadata and cache key. + + Examples: + >>> # Get real-time greeks during market hours + >>> greek_mgr = GreekDataManager("AAPL") + >>> result = greek_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + >>> print(f"Delta: {result.timeseries['delta'].iloc[0]:.4f}") + + >>> # Get only delta and vega in real-time + >>> from trade.datamanager._enums import GreekType + >>> result = greek_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... greeks_to_compute=[GreekType.DELTA, GreekType.VEGA] + ... ) + + >>> # Use last available if market closed + >>> from trade.datamanager._enums import RealTimeFallbackOption + >>> result = greek_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... fallback_option=RealTimeFallbackOption.USE_LAST_AVAILABLE + ... ) + """ + + res = self.get_at_time_greeks( + as_of=datetime.now(), + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + greeks_to_compute=greeks_to_compute, + S=S, + r=r, + d=d, + f=f, + vol=vol, + endpoint_source=OptionSpotEndpointSource.QUOTE, + market_model=market_model, + undo_adjust=undo_adjust, + fallback_option=fallback_option, + model_price=model_price, + ) + res.rt = True + return res + + def _create_load_request( + ## Requied parameters to ensure correct data is loaded + self, + start_date: DATE_HINT, + end_date: DATE_HINT, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: DivType, + market_model: OptionPricingModel, + endpoint_source: OptionSpotEndpointSource, + model_price: ModelPrice, + *, + ## Optional pre-loaded data. If not provided, will be loaded. + s: Optional[SpotResult] = None, + r: Optional[RatesResult] = None, + f: Optional[ForwardResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + undo_adjust: bool = True, + ) -> LoadRequest: + """Create a LoadRequest specifying which market data to load for greek calculation. + + Internal utility that determines which data sources need to be loaded based on: + 1. Which data is already provided (pre-loaded) + 2. Which pricing model is being used (BSM needs forwards, binomial needs spot) + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + market_model: Pricing model (BSM or BINOMIAL). + endpoint_source: Option data source (ORATS, HIST, QUOTE). + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). + s: Optional pre-loaded spot data. If None, will be loaded. + r: Optional pre-loaded rates data. If None, will be loaded. + f: Optional pre-loaded forward data. If None, will be loaded (BSM only). + d: Optional pre-loaded dividend data. If None, will be loaded. + vol: Optional pre-loaded volatility data. If None, will be loaded. + undo_adjust: If True, uses split-adjusted prices. + + Returns: + LoadRequest object with flags indicating which data sources to load. + + Examples: + >>> # Internal usage - creates request to load all data + >>> request = greek_mgr._create_load_request( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE, + ... market_model=OptionPricingModel.BSM, + ... endpoint_source=OptionSpotEndpointSource.HIST, + ... model_price=ModelPrice.CLOSE + ... ) + >>> # request.load_forward = True (BSM needs forwards) + >>> # request.load_spot = True (no spot provided) + >>> # request.load_vol = True (no vol provided) + """ + + req = LoadRequest( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + vol_model=VolatilityModel.MARKET, + model_price=model_price, + market_model=market_model, + ## Load spot only if missing. + load_spot=(s is None), + ## Load forward only if missing and using BSM model. Binomial uses spot price. + load_forward=(market_model == OptionPricingModel.BSM) and (f is None), + load_vol=(vol is None), + load_dividend=(d is None), + load_rates=(r is None), + ## Not needed for greek calculation + load_option_spot=False, + undo_adjust=undo_adjust, + ) + return req diff --git a/trade/datamanager/guide.txt b/trade/datamanager/guide.txt index 0aa8bab..ab2ff91 100644 --- a/trade/datamanager/guide.txt +++ b/trade/datamanager/guide.txt @@ -1,212 +1,720 @@ -======================================== -OPTION DATA MANAGER – TOP LEVEL DESIGN -======================================== - ----------------------------------------- -AS-OF POLICY (SIMPLIFIED) ----------------------------------------- - -As-of represents the assumed market state time for a calculation. - -We use ONLY three modes: - -1) intraday - - Historical intraday timestamp - - Used for backtests and intraday analytics - - Example: 2025-01-04 10:32:15 - -2) eod - - End-of-day snapshot - - Used for daily data, surfaces, storage - - Example: 2025-01-04 EOD - -3) snap - - Real-time / near-real-time snapshot - - Used for live trading - - Example: now() with short TTL - -As-of is NOT an interval or bar size. -As-of is part of every cache key. - ----------------------------------------- -CORE ARCHITECTURE OVERVIEW ----------------------------------------- - -DESIGN PHILOSOPHY: -Facade + domain managers + pluggable engines -Read-through caching + materialization - ----------------------------------------- -HEAD / ORCHESTRATION ----------------------------------------- - -DataManager -- Single entrypoint -- Coordinates all domain managers -- No heavy computation -- Enforces lifecycle + consistency - ----------------------------------------- -DOMAIN MANAGERS (CORE PRODUCTS) ----------------------------------------- - -MarketSnapshotManager -- Owns as-of normalization -- Produces snapshot identity (hash or id) - -SpotManager -- Spot prices -- Cached by (underlying, as_of) - -RatesManager -- Discount factors / rate curves - -DividendManager -- Dividend schedules / borrow - -ForwardManager -- Produces forward from spot + carry -- Cached by (underlying, expiry, as_of) - -ChainManager -- Option metadata + quotes -- Cached by (underlying, as_of) - -VolManager -- Entry point for volatility -- Routes to engines -- Owns vol caching + provenance - -GreeksManager -- Entry point for greeks -- Requests vols explicitly from VolManager - ----------------------------------------- -ENGINE LAYER (INTERNAL ONLY) ----------------------------------------- - -Volatility Engines: -- BSVolEngine -- CRRVolEngine -- SurfaceVolEngine -- FitEngine - -Greeks Engines: -- BSGreeksEngine -- NumericalGreeksEngine -- BinomialGreeksEngine - -Engines are NEVER called directly by strategies. - ----------------------------------------- -REQUEST / RESULT CONTRACTS ----------------------------------------- - -Requests: -- SpotRequest -- ChainRequest -- VolRequest -- GreeksRequest - -Results: -- SpotResult -- VolResult -- GreeksResult - -Each result contains: -- values (Series / DataFrame) -- provenance (engine, config hash, snapshot id, as_of) -- diagnostics (optional) - ----------------------------------------- -CACHING LAYER ----------------------------------------- - -CustomCache -- Disk-backed -- Used by all managers - -CacheKey -- Standardized key builder -- Includes: - - identity (option / underlying) - - artifact type - - as_of - - engine / version info - ----------------------------------------- -PERSISTENCE / OFFLOAD ----------------------------------------- - -offload() -- Base capability on managers -- Cron-driven - -Materializer (optional) -- Shared SQL writer -- Batching, retries, idempotency - ----------------------------------------- -POLICIES & DIAGNOSTICS ----------------------------------------- - -AsOfPolicy -- intraday / eod / snap - -PricePolicy -- mid / bid / ask - -CachePolicy -- TTL / refresh rules - -Metrics / Logging -- cache hit/miss -- compute time -- engine selection - ----------------------------------------- -BUILD ORDER (DO THIS IN ORDER) ----------------------------------------- - -PHASE 1 – FOUNDATION -- CustomCache -- CacheKey -- AsOfPolicy - -PHASE 2 – MARKET DATA -- ChainManager -- SpotManager - -PHASE 3 – CARRY -- RatesManager -- DividendManager -- ForwardManager - -PHASE 4 – VOLATILITY -- VolManager -- BSVolEngine -- VolRequest / VolResult - -PHASE 5 – GREEKS -- GreeksManager -- BSGreeksEngine -- GreeksRequest / GreeksResult - -PHASE 6 – PERSISTENCE -- offload() -- Materializer - -PHASE 7 – EXTENSIONS -- CRRVolEngine -- SurfaceVolEngine -- FitEngine -- Batch / chain analytics - ----------------------------------------- -GOLDEN RULES ----------------------------------------- - -1) Strategies never choose engines -2) Managers choose engines -3) Cache keys always include as_of -4) Provenance travels with data + ================================================================================ +QUANTTOOLS DATAMANAGER MODULE – COMPREHENSIVE GUIDE +================================================================================ + +================================================================================ +1. MODULE OVERVIEW +================================================================================ + +The datamanager module provides a complete data infrastructure for quantitative +options trading and backtesting. It handles market data retrieval, caching, +processing, and calculation of derived metrics (forwards, volatilities, greeks, +theoretical prices). + +DESIGN PRINCIPLES: +- Singleton pattern per symbol for efficient resource management +- Intelligent multi-tier caching (memory + disk with expiration) +- Automatic data loading from multiple sources (ThetaData, OpenBB, YFinance) +- Type-safe result containers with full metadata +- Consistent API across all managers + +KEY CAPABILITIES: +- Historical and real-time market data access +- Split adjustment handling for backtesting +- Dividend schedule construction (discrete/continuous) +- Forward price computation with carry models +- Implied volatility calculation (BSM, Binomial) +- Greek calculation with multiple models +- Theoretical pricing and scenario analysis + + +================================================================================ +2. ARCHITECTURE OVERVIEW +================================================================================ + +COMPONENT HIERARCHY: + +┌─────────────────────────────────────────────────────────────────────┐ +│ BaseDataManager (ABC) │ +│ - Cache management (CustomCache) │ +│ - Key construction (namespaced, artifact-based) │ +│ - Configuration (OptionDataConfig singleton) │ +│ - Logger setup │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────┴───────────────────┐ + │ │ + ┌───────────▼─────────────┐ ┌─────────────▼──────────────┐ + │ Market Data Layer │ │ Derived Metrics Layer │ + │ │ │ │ + │ - SpotDataManager │ │ - ForwardDataManager │ + │ - RatesDataManager │ │ - VolDataManager │ + │ - DividendDataManager │ │ - GreekDataManager │ + │ - OptionSpotDataManager │ │ - TheoDataFunctions │ + │ - MarketTimeseries │ │ (get_option_theo_price) │ + └─────────────────────────┘ └────────────────────────────┘ + + +================================================================================ +3. CORE DATA MANAGERS +================================================================================ + +-------------------------------------------------------------------------------- +3.1 SpotDataManager +-------------------------------------------------------------------------------- +Manages underlying equity spot prices with split adjustment support. + +SINGLETON: Yes (per symbol) +CACHE: 45-day expiration +DATA SOURCE: MarketTimeseries (OpenBB/YFinance) + +KEY METHODS: + get_spot_timeseries(start, end, undo_adjust=True) -> SpotResult + - Returns daily closing prices + - undo_adjust=True: split-adjusted chain_spot + - undo_adjust=False: unadjusted spot + + get_spot(date, undo_adjust=True) -> SpotResult + - Single date spot price + + rt(undo_adjust=True) -> float + - Real-time spot price + +TYPICAL USAGE: + spot_mgr = SpotDataManager("AAPL") + result = spot_mgr.get_spot_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + undo_adjust=True + ) + prices = result.daily_spot # pd.Series with DatetimeIndex + +-------------------------------------------------------------------------------- +3.2 RatesDataManager +-------------------------------------------------------------------------------- +Manages risk-free interest rates from US Treasury bills (^IRX). + +SINGLETON: Yes (global - no symbol) +CACHE: 30-day expiration +DATA SOURCE: YFinance (13-week T-Bill) + +KEY METHODS: + get_risk_free_rate_timeseries(start, end) -> RatesResult + - Returns daily risk-free rates (annualized) + - Automatically handles missing dates (forward fill) + + get_rate(date) -> RatesResult + - Single date rate + + rt() -> float + - Real-time rate + +TYPICAL USAGE: + rates_mgr = RatesDataManager() + result = rates_mgr.get_risk_free_rate_timeseries( + start_date="2025-01-01", + end_date="2025-01-31" + ) + rates = result.daily_risk_free_rates # pd.Series + +-------------------------------------------------------------------------------- +3.3 DividendDataManager +-------------------------------------------------------------------------------- +Manages dividend data with schedule construction for option pricing. + +SINGLETON: Yes (per symbol) +CACHE: 60-day expiration (+ temp cache for short-lived data) +DATA SOURCE: MarketTimeseries + forecasting + +KEY METHODS: + get_schedule_timeseries(start, end, maturity, div_type, undo_adjust) -> DividendsResult + - Returns daily Schedule objects (discrete) or yields (continuous) + - Builds forward-looking schedules for each valuation date + - Handles split adjustments + - Supports partial caching with smart merging + + get_discrete_dividend_schedule(start, end) -> Tuple[Schedule, str] + - Raw dividend schedule for date range + - Used internally by other managers + +TYPICAL USAGE: + div_mgr = DividendDataManager("AAPL") + result = div_mgr.get_schedule_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + maturity_date="2025-06-20", + dividend_type=DivType.DISCRETE, + undo_adjust=True + ) + schedules = result.daily_discrete_dividends # pd.Series of Schedule objects + +-------------------------------------------------------------------------------- +3.4 ForwardDataManager +-------------------------------------------------------------------------------- +Computes forward prices using cost-of-carry models. + +SINGLETON: Yes (per symbol) +CACHE: 30-day expiration +DEPENDENCIES: SpotDataManager, RatesDataManager, DividendDataManager + +KEY METHODS: + get_forward_timeseries(start, end, maturity, div_type, use_chain_spot) -> ForwardResult + - Computes daily forward prices to fixed maturity + - Discrete model: F = S * exp(r*T) - PV(dividends) + - Continuous model: F = S * exp((r-q)*T) + + get_forward(date, maturity, div_type, use_chain_spot) -> ForwardResult + - Single date forward + +TYPICAL USAGE: + fwd_mgr = ForwardDataManager("AAPL") + result = fwd_mgr.get_forward_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + maturity_date="2025-06-20", + dividend_type=DivType.DISCRETE, + use_chain_spot=True + ) + forwards = result.daily_discrete_forward # pd.Series + +-------------------------------------------------------------------------------- +3.5 OptionSpotDataManager +-------------------------------------------------------------------------------- +Retrieves option contract market prices from ThetaData API. + +SINGLETON: No (per symbol) +CACHE: 7-day expiration +DATA SOURCE: ThetaData (EOD or Quote endpoint) + +KEY METHODS: + get_option_spot_timeseries(start, end, strike, expiration, right, endpoint_source) -> OptionSpotResult + - Returns OHLC data for option contract + - endpoint_source=EOD: end-of-day report + - endpoint_source=QUOTE: intraday quotes + + get_option_spot(date, strike, expiration, right) -> OptionSpotResult + - Single date option price + +TYPICAL USAGE: + opt_mgr = OptionSpotDataManager("AAPL") + result = opt_mgr.get_option_spot_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="C", + endpoint_source=OptionSpotEndpointSource.EOD + ) + ohlc = result.daily_option_spot # pd.DataFrame with [open, high, low, close] + +-------------------------------------------------------------------------------- +3.6 MarketTimeseries +-------------------------------------------------------------------------------- +Central market data repository with lazy loading and caching. + +SINGLETON: Yes (global instance) +CACHE: Multi-tier (memory + disk) +DATA SOURCE: OpenBB, ThetaData, YFinance + +KEY FEATURES: +- Loads all market data for a symbol on first request +- Maintains timeseries for: spot, chain_spot, dividends, rates +- Provides point-in-time snapshots (AtIndexResult) +- Handles corporate actions (splits, dividends) +- Thread-safe access + +TYPICAL USAGE: + from trade.datamanager.vars import TS # Global instance + TS.load("AAPL") # Lazy load on first access + data = TS.at_index("AAPL", "2025-01-15") + spot = data.spot # pd.Series with OHLCV + + +================================================================================ +4. DERIVED METRICS MANAGERS +================================================================================ + +-------------------------------------------------------------------------------- +4.1 VolDataManager +-------------------------------------------------------------------------------- +Computes implied volatilities from option market prices. + +SINGLETON: Yes (per symbol) +CACHE: 7-day expiration +MODELS: Black-Scholes-Merton (BSM), Cox-Ross-Rubinstein (CRR) + +KEY METHODS: + get_implied_volatility_timeseries(start, end, strike, expiration, right, + model, american, dividend_type, n_steps, ...) -> VolatilityResult + - Computes IV for each date in range + - BSM: Fast, European-style only + - BINOMIAL: CRR tree, supports American exercise + - EURO_EQIV: Converts American IV to European equivalent + + rt(strike, expiration, right, ...) -> float + - Real-time implied volatility + +TYPICAL USAGE: + vol_mgr = VolDataManager("AAPL") + result = vol_mgr.get_implied_volatility_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="c", + model=OptionPricingModel.BSM, + dividend_type=DivType.DISCRETE + ) + ivs = result.timeseries # pd.Series of implied vols + +-------------------------------------------------------------------------------- +4.2 GreekDataManager +-------------------------------------------------------------------------------- +Computes option sensitivities (delta, gamma, vega, theta, rho). + +SINGLETON: Yes (per symbol) +CACHE: 7-day expiration +MODELS: Black-Scholes (analytical), Binomial (numerical) + +KEY METHODS: + get_greeks_timeseries(start, end, strike, expiration, right, + greeks_to_compute, model, american, ...) -> GreekResultSet + - Computes specified greeks for each date + - greeks_to_compute: list of GreekType (optional, defaults to all) + - Returns DataFrame with columns for each greek + + rt(strike, expiration, right, ...) -> GreekResultSet + - Real-time greeks + +TYPICAL USAGE: + greek_mgr = GreekDataManager("AAPL") + result = greek_mgr.get_greeks_timeseries( + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="c", + greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA] + ) + greeks = result.timeseries # pd.DataFrame with [delta, gamma, ...] + +-------------------------------------------------------------------------------- +4.3 Theoretical Pricing Functions +-------------------------------------------------------------------------------- +Module-level functions for option theoretical pricing and scenario analysis. + +KEY FUNCTIONS: + get_option_theoretical_price(symbol, start, end, strike, expiration, right, + market_model, dividend_type, american, ...) -> TheoreticalPriceResult + - Computes theoretical option prices using specified model + - Loads all required market data automatically + - Returns timeseries of prices + + calculate_scenarios(symbol, as_of, strike, expiration, right, + spot_scenarios, vol_scenarios, return_pnl, ...) -> ScenariosResult + - Runs scenario analysis (stress testing) + - spot_scenarios: list of spot multipliers (e.g., [0.9, 1.0, 1.1]) + - vol_scenarios: list of vol adjustments (e.g., [-0.05, 0.0, 0.05]) + - Returns grid of prices/PnL across scenarios + +TYPICAL USAGE: + from trade.datamanager.theo import get_option_theoretical_price, calculate_scenarios + + # Theoretical pricing + theo_result = get_option_theoretical_price( + symbol="AAPL", + start_date="2025-01-01", + end_date="2025-01-31", + strike=150.0, + expiration="2025-06-20", + right="c", + market_model=OptionPricingModel.BSM, + dividend_type=DivType.DISCRETE + ) + prices = theo_result.timeseries + + # Scenario analysis + scenarios = calculate_scenarios( + symbol="AAPL", + as_of="2025-01-15", + strike=150.0, + expiration="2025-06-20", + right="c", + spot_scenarios=[0.95, 1.0, 1.05], + vol_scenarios=[-0.05, 0.0, 0.05], + return_pnl=True + ) + grid = scenarios.grid # pd.DataFrame with spot x vol grid + + +================================================================================ +5. RESULT CONTAINERS +================================================================================ + +All managers return strongly-typed Result objects (dataclasses): + +Result (Base) + - model_input_keys: Dict of inputs used + - rt: bool flag for real-time data + - fallback_option: How to handle missing real-time data + +SpotResult + - timeseries: pd.Series (renamed to daily_spot via property) + - symbol: str + - undo_adjust: bool + +RatesResult + - timeseries: pd.Series (renamed to daily_risk_free_rates) + - key: cache key + +DividendsResult + - timeseries: pd.Series of Schedule objects or yields + - dividend_type: DivType (DISCRETE or CONTINUOUS) + - undo_adjust: bool + - Properties: daily_discrete_dividends, daily_continuous_dividends + +ForwardResult + - timeseries: pd.Series (renamed based on div_type) + - dividend_type: DivType + - dividend_result: DividendsResult used + - Properties: daily_discrete_forward, daily_continuous_forward + +OptionSpotResult + - timeseries: pd.DataFrame (renamed to daily_option_spot) + - strike, expiration, right, symbol + - endpoint_source: OptionSpotEndpointSource + +VolatilityResult + - timeseries: pd.Series of implied vols + - model: OptionPricingModel + - volatility_model: VolatilityModel + - strike, expiration, right, symbol + +GreekResultSet + - timeseries: pd.DataFrame with columns for each greek + - greeks_computed: List[GreekType] + - strike, expiration, right, symbol + + +================================================================================ +6. CONFIGURATION & ENUMS +================================================================================ + +-------------------------------------------------------------------------------- +OptionDataConfig (Singleton) +-------------------------------------------------------------------------------- +Global configuration for all datamanagers. + +KEY SETTINGS: + - option_spot_endpoint_source: EOD or QUOTE + - dividend_type: DISCRETE or CONTINUOUS + - option_model: BSM, BINOMIAL, EURO_EQIV + - volatility_model: MARKET or MODEL_DYNAMIC + - n_steps: int (binomial tree steps) + - undo_adjust: bool (use split-adjusted prices) + - model_price: MIDPOINT, BID, ASK, OPEN, CLOSE + - real_time_fallback_option: USE_LAST_AVAILABLE, RAISE_ERROR, ZEROED, NAN + +ACCESS: + from trade.datamanager.config import OptionDataConfig + config = OptionDataConfig() + config.n_steps = 200 # Modify globally + +-------------------------------------------------------------------------------- +Key Enumerations +-------------------------------------------------------------------------------- + +Interval: + - INTRADAY: historical intraday timestamp + - EOD: end-of-day daily snapshot + - NA: not applicable + +SeriesId: + - HIST: historical timeseries + - AT_TIME: single point-in-time + - SNAPSHOT: real-time snapshot + +ArtifactType: + - SPOT, CHAIN, RATES, DIVS, FWD, OPTION_SPOT + - IV, TVAR + - GREEKS, DELTA, GAMMA, VEGA, THETA, RHO, VOLGA, VANNA + +GreekType: + - DELTA, GAMMA, VEGA, THETA, RHO, VOLGA, VANNA + +OptionPricingModel: + - BSM: Black-Scholes-Merton (fast, European) + - BINOMIAL: CRR tree (slower, American) + - EURO_EQIV: European equivalent + +VolatilityModel: + - MARKET: implied from market prices + - MODEL_DYNAMIC: computed from model + +DivType (from optionlib): + - DISCRETE: schedule-based dividends + - CONTINUOUS: yield-based dividends + +ModelPrice: + - MIDPOINT, BID, ASK, OPEN, CLOSE + +OptionSpotEndpointSource: + - EOD: end-of-day report (available after 6pm ET) + - QUOTE: intraday quote endpoint + + +================================================================================ +7. CACHING SYSTEM +================================================================================ + +-------------------------------------------------------------------------------- +Cache Architecture +-------------------------------------------------------------------------------- + +THREE-TIER CACHING: +1. Memory Cache (CustomCache in-memory dict) + - Fastest access + - Per-manager instance + - Cleared on process exit + +2. Disk Cache (CustomCache pickle files) + - Persistent across sessions + - Configurable expiration (7-60 days typical) + - Per-manager, per-symbol + +3. Partial Cache Merging + - Detects missing dates in cache + - Fetches only missing data + - Merges with existing cache + - Reduces API calls + +-------------------------------------------------------------------------------- +CacheSpec Configuration +-------------------------------------------------------------------------------- +Controls cache behavior per manager: + +@dataclass(frozen=True, slots=True) +class CacheSpec: + base_dir: Optional[Path] = DM_GEN_PATH # Cache directory + default_expire_days: Optional[int] = 500 # Full cache expiration + default_expire_seconds: Optional[int] = None # Entry expiration + cache_fname: Optional[str] = None # Cache filename + clear_on_exit: bool = False # Auto-clear on exit + +USAGE: + Each manager defines class-level CACHE_SPEC: + CACHE_SPEC: CacheSpec = CacheSpec( + cache_fname="spot_data_manager", + default_expire_days=45 + ) + +-------------------------------------------------------------------------------- +Cache Keys +-------------------------------------------------------------------------------- +Constructed using construct_cache_key() utility: + +KEY COMPONENTS: + - symbol: underlying ticker + - artifact_type: ArtifactType enum + - series_id: SeriesId enum + - interval: Interval enum + - namespace: optional isolation + - Additional metadata (strike, expiration, model, etc.) + +EXAMPLE KEY: + "AAPL__hist__eod__spot__undo_True" + "AAPL__hist__eod__iv__K150.0_exp20250620_rc_model_bsm" + + +================================================================================ +8. DATE HANDLING +================================================================================ + +-------------------------------------------------------------------------------- +Date Conversion (CRITICAL) +-------------------------------------------------------------------------------- +ALWAYS use to_datetime from trade.helpers.helper: + +from trade.helpers.helper import to_datetime + +# Handles strings, datetime objects, and iterables +date_obj = to_datetime("2025-01-15") +dates = to_datetime(["2025-01-15", "2025-01-16"]) + +NEVER use: + - datetime.strptime() + - pd.to_datetime() directly + +-------------------------------------------------------------------------------- +Date Synchronization +-------------------------------------------------------------------------------- +Managers automatically synchronize dates with available data: + +- is_available_on_date(): Checks if data exists for date +- _sync_date(): Adjusts requested range to available range +- change_to_last_busday(): Converts to last business day +- get_missing_dates(): Identifies gaps in cache + +================================================================================ +9. TYPICAL WORKFLOWS +================================================================================ + +-------------------------------------------------------------------------------- +9.1 Backtesting Options Strategy +-------------------------------------------------------------------------------- +from trade.datamanager import ( + SpotDataManager, DividendDataManager, ForwardDataManager, + VolDataManager, GreekDataManager +) +from trade.optionlib.config.types import DivType +from trade.datamanager._enums import OptionPricingModel, GreekType + +symbol = "AAPL" +start, end = "2025-01-01", "2025-01-31" +strike, expiration, right = 150.0, "2025-06-20", "c" + +# 1. Load spot prices +spot_mgr = SpotDataManager(symbol) +spot_result = spot_mgr.get_spot_timeseries(start, end, undo_adjust=True) +spots = spot_result.daily_spot + +# 2. Get implied volatilities +vol_mgr = VolDataManager(symbol) +vol_result = vol_mgr.get_implied_volatility_timeseries( + start, end, strike, expiration, right, + model=OptionPricingModel.BSM, + dividend_type=DivType.DISCRETE +) +ivs = vol_result.timeseries + +# 3. Compute greeks +greek_mgr = GreekDataManager(symbol) +greek_result = greek_mgr.get_greeks_timeseries( + start, end, strike, expiration, right, + greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.VEGA] +) +greeks = greek_result.timeseries # DataFrame with delta, gamma, vega + +# 4. Run scenarios for risk management +from trade.datamanager.theo import calculate_scenarios +scenarios = calculate_scenarios( + symbol=symbol, + as_of="2025-01-15", + strike=strike, + expiration=expiration, + right=right, + spot_scenarios=[0.95, 1.0, 1.05], + vol_scenarios=[-0.05, 0.0, 0.05] +) +print(scenarios.grid) + +-------------------------------------------------------------------------------- +9.2 Real-Time Option Monitoring +-------------------------------------------------------------------------------- +from trade.datamanager import VolDataManager, GreekDataManager + +symbol = "AAPL" +strike, expiration, right = 150.0, "2025-06-20", "c" + +# Get real-time IV +vol_mgr = VolDataManager(symbol) +current_iv = vol_mgr.rt(strike, expiration, right) + +# Get real-time greeks +greek_mgr = GreekDataManager(symbol) +greek_result = greek_mgr.rt(strike, expiration, right) +delta = greek_result.timeseries["delta"].iloc[0] +gamma = greek_result.timeseries["gamma"].iloc[0] + +print(f"IV: {current_iv:.4f}, Delta: {delta:.4f}, Gamma: {gamma:.6f}") + + +================================================================================ +10. BEST PRACTICES & GOTCHAS +================================================================================ + +DO: +✓ Use singleton managers - they cache internally +✓ Use to_datetime() for all date conversions +✓ Provide DividendsResult to ForwardDataManager to avoid re-fetching +✓ Use undo_adjust=True for backtesting (split-adjusted prices) +✓ Specify greeks_to_compute to reduce computation time +✓ Use model_price=MIDPOINT for fair value calculations +✓ Check result.is_empty() before using data +✓ Use rt() methods for real-time data +✓ Let managers handle data loading automatically + +DON'T: +✗ Create multiple instances of same symbol manager +✗ Use datetime.strptime() or pd.to_datetime() directly +✗ Mix undo_adjust=True/False in same calculation +✗ Ignore dividend_type when comparing prices +✗ Call _private_methods() directly +✗ Modify OptionDataConfig after initialization +✗ Assume cache is always warm - check for empty results +✗ Use BSM model for American options pricing + +COMMON ISSUES: +- "Data not available": Check date range with is_available_on_date() +- "Cache miss": Normal on first run, subsequent runs will hit cache +- "IV solver failed": Option may be deep ITM/OTM or have bad data +- "Mismatched undo_adjust": Ensure consistent split adjustment across managers + + +================================================================================ +11. UTILITY MODULES +================================================================================ + +utils/ + - model.py: Model data loading (_load_model_data_timeseries, LoadRequest) + - vol_helpers.py: Volatility calculation helpers + - greeks_helpers.py: Greek calculation helpers + - date.py: Date utilities (sync_date_index, is_available_on_date) + - cache.py: Cache utilities (_data_structure_cache_it) + - data_structure.py: Data structure validation + - logging.py: Logging configuration + - enums_utils.py: Cache key construction (construct_cache_key) + +market_data_helpers/ + - spot.py: Spot price loading from OpenBB + - Additional helper functions for data retrieval + + +================================================================================ +12. EXTENSION POINTS +================================================================================ + +To add a new manager: +1. Inherit from BaseDataManager +2. Define CACHE_NAME (unique string) +3. Define CACHE_SPEC (CacheSpec instance) +4. Define DEFAULT_SERIES_ID (SeriesId enum) +5. Implement __init__ with singleton pattern if needed +6. Add methods returning Result subclass +7. Use self.cache.get() / self.cache.set() for caching +8. Use construct_cache_key() for key generation + +Example skeleton: + class MyDataManager(BaseDataManager): + CACHE_NAME: str = "my_data_manager" + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + DEFAULT_SERIES_ID: SeriesId = SeriesId.HIST + + def __init__(self, symbol: str): + super().__init__(symbol=symbol) + self.symbol = symbol + + def get_my_data(self, start, end) -> MyResult: + key = construct_cache_key(...) + cached = self.cache.get(key) + if cached: + return cached + # Fetch data + result = MyResult(...) + self.cache.set(key, result) + return result + + +================================================================================ +END OF GUIDE +================================================================================ diff --git a/trade/datamanager/loaders.py b/trade/datamanager/loaders.py new file mode 100644 index 0000000..11fbeba --- /dev/null +++ b/trade/datamanager/loaders.py @@ -0,0 +1,222 @@ +"""Convenient loader functions for comprehensive option data retrieval. + +This module provides high-level loader functions that simplify fetching complete +option data packages including spot, forward, dividend, vol, greeks, and rates. +Functions handle parameter validation, date conversion, and coordinate data loading +across multiple DataManagers. + +Key Features: + - One-call option data loading (all dependencies included) + - Automatic parameter validation and date conversion + - Support for timeseries, single-date, and real-time modes + - Configurable pricing models and dividend treatments + - Returns unified ModelResultPack with all data components + +Typical Usage: + >>> from trade.datamanager.loaders import load_full_option_data + >>> from trade.datamanager._enums import DivType + >>> + >>> # Load historical option data with all dependencies + >>> pack = load_full_option_data( + ... symbol="AAPL", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... dividend_type=DivType.DISCRETE + ... ) + >>> + >>> # Access individual components + >>> greeks = pack.greek.timeseries + >>> vol = pack.vol.timeseries + >>> spot = pack.spot.timeseries + >>> + >>> # Real-time mode + >>> rt_pack = load_full_option_data( + ... symbol="AAPL", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... rt=True + ... ) +""" + +from typing import Optional +from trade.datamanager.result import DividendsResult, SpotResult +from trade.datamanager.utils.model import LoadRequest, _load_model_data_timeseries, ModelResultPack +from trade.datamanager.utils.date import DATE_HINT +from trade.datamanager._enums import ( + SeriesId, + OptionSpotEndpointSource, + VolatilityModel, + OptionPricingModel, + ModelPrice, + DivType, +) +from trade.helpers.helper import to_datetime +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.logging import get_logging_level, register_to_factor_list + +logger = setup_logger("trade.datamanager.loaders", stream_log_level=get_logging_level()) +register_to_factor_list("trade.datamanager.loaders") + + +def load_full_option_data( + symbol: str, + *, + expiration: DATE_HINT, + strike: float, + right: str, + start_date: DATE_HINT = None, + end_date: DATE_HINT = None, + as_of: DATE_HINT = None, + rt: bool = False, + ## Optional parameters. If not passed will refer to global defaults found in OptionConfig + series_id: SeriesId = None, + dividend_type: DivType = None, + endpoint_source: OptionSpotEndpointSource = None, + vol_model: VolatilityModel = None, + market_model: OptionPricingModel = None, + model_price: ModelPrice = None, + + ## Optional data for modelling. + spot_timeseries: Optional[SpotResult] = None, + dividend_timeseries: Optional[DividendsResult] = None, + forward_timeseries: Optional[SpotResult] = None, + option_spot_timeseries: Optional[SpotResult] = None, + vol_timeseries: Optional[SpotResult] = None, + greek_timeseries: Optional[SpotResult] = None, + rates_timeseries: Optional[SpotResult] = None, +) -> ModelResultPack: + """Load comprehensive option data including spot, forward, vol, greeks, and rates. + + Convenience function that loads all required data for option analysis in a single + call. Automatically handles data dependencies, caching, and model selection. Supports + three modes: timeseries (start/end dates), single date (as_of), and real-time (rt). + + Args: + symbol: Equity symbol (e.g., "AAPL", "MSFT") + expiration: Option expiration date (YYYY-MM-DD string or datetime) + strike: Option strike price + right: Option type - "call"/"c" or "put"/"p" + start_date: Start of timeseries range (YYYY-MM-DD string or datetime) + end_date: End of timeseries range (YYYY-MM-DD string or datetime) + as_of: Single date for historical snapshot (YYYY-MM-DD string or datetime) + rt: If True, load real-time data + series_id: Option series identifier (default from OptionConfig) + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS (default from OptionConfig) + endpoint_source: Data source for option prices (default from OptionConfig) + vol_model: Volatility calculation model (default from OptionConfig) + market_model: Option pricing model (BSM, CRR, etc., default from OptionConfig) + model_price: Model price type (default from OptionConfig) + + Returns: + ModelResultPack containing: + - spot: SpotResult with underlying prices + - forward: ForwardResult with forward prices + - dividend: DividendsResult with dividend schedules + - rates: RatesResult with risk-free rates + - option_spot: OptionSpotResult with market option prices + - vol: VolatilityResult with implied volatilities + - greek: GreekResultSet with option sensitivities + + Raises: + ValueError: If mode specification is ambiguous (e.g., both start_date and as_of provided) + + Examples: + >>> # Historical timeseries + >>> pack = load_full_option_data( + ... symbol="AAPL", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... start_date="2025-01-01", + ... end_date="2025-01-31" + ... ) + >>> print(pack.greek.timeseries.delta.head()) + datetime + 2025-01-02 0.5234 + 2025-01-03 0.5301 + ... + + >>> # Single date snapshot + >>> pack = load_full_option_data( + ... symbol="AAPL", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... as_of="2025-01-15" + ... ) + >>> print(f"Vol on 2025-01-15: {pack.vol.as_of_value:.4f}") + + >>> # Real-time + >>> pack = load_full_option_data( + ... symbol="AAPL", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call", + ... rt=True + ... ) + >>> print(f"Current delta: {pack.greek.rt_value.delta:.4f}") + + Notes: + - Only one mode should be specified: (start_date, end_date), as_of, or rt + - All optional parameters default to values in OptionConfig + - Data is automatically cached for efficient repeated access + - Uses split-adjusted prices (undo_adjust=True) by default + """ + if start_date and end_date: + ts_start = to_datetime(start_date) + ts_end = to_datetime(end_date) + as_of = None + rt = False + + elif as_of: + ts_start = None + ts_end = None + as_of = to_datetime(as_of) + rt = False + + elif rt: + ts_start = None + ts_end = None + as_of = None + rt = True + + request = LoadRequest( + symbol=symbol, + start_date=ts_start, + end_date=ts_end, + as_of=as_of, + expiration=expiration, + strike=strike, + right=right, + series_id=series_id, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + vol_model=vol_model, + market_model=market_model, + model_price=model_price, + load_spot=True, + load_dividend=True, + load_forward=True, + load_option_spot=True, + load_vol=True, + load_greek=True, + load_rates=True, + undo_adjust=True, + rt=rt, + + ## Provided data (if any) + spot_timeseries=spot_timeseries, + dividend_timeseries=dividend_timeseries, + forward_timeseries=forward_timeseries, + option_spot_timeseries=option_spot_timeseries, + vol_timeseries=vol_timeseries, + greek_timeseries=greek_timeseries, + rates_timeseries=rates_timeseries, + ) + + data_packet = _load_model_data_timeseries(request) + return data_packet diff --git a/trade/datamanager/market_data.py b/trade/datamanager/market_data.py new file mode 100644 index 0000000..af6410a --- /dev/null +++ b/trade/datamanager/market_data.py @@ -0,0 +1,1268 @@ +"""Market Data Management and Timeseries Infrastructure. + +This module provides comprehensive market data loading, caching, and retrieval +infrastructure for options backtesting and live trading. It manages spot prices, +chain data, dividends, risk-free rates, and custom market indicators with +intelligent caching strategies for performance optimization. + +Core Classes: + MarketTimeseries: Main container for all market data with lazy loading + TimeseriesData: Structured holder for symbol-specific timeseries + AtIndexResult: Point-in-time snapshot of market data for a symbol + +Key Features: + - Multi-source data retrieval (OpenBB, ThetaData, YFinance) + - Hierarchical caching system (memory, disk, persistent) + - Automatic data refresh with configurable intervals + - Corporate action awareness (splits, dividends) + - Custom data integration via user-defined callables + - Thread-safe access with proper locking + - Signal handlers for cleanup on exit + +Data Types Managed: + Spot Prices (Equity OHLCV): + - Open, high, low, close prices + - Volume and trading activity + - Adjusted for splits and dividends + - Sourced from OpenBB/YFinance + + Chain Spot Prices: + - Underlying prices from option chain data + - Used for option pricing consistency + - May differ from equity spot due to timing + - Sourced from ThetaData + + Dividends: + - Regular dividend timeseries + - Special dividends with ex-dates + - Used for American option pricing + - Affects early exercise decisions + + Risk-Free Rates: + - Treasury yield curve (multiple tenors) + - Interpolated rates for option pricing + - Daily updates from Fed data + - Annualized rate convention + + Additional Data (Custom): + - User-defined indicators + - Market regime indicators + - Volatility surfaces + - Sentiment data + +Caching Architecture: + Three-Tier System: + 1. Memory Cache (Fastest): + - In-memory dictionaries + - No expiration during session + - Cleared on exit + + 2. Disk Cache (Fast): + - CustomCache with pickle serialization + - 30-minute to 45-day expiration + - Per-symbol and per-data-type + + 3. Persistent Cache: + - Long-term storage for historical data + - Survives process restarts + - Used for backtesting data + + Cache Keys: + - Spot: SPOT_CACHE (45-day expiration) + - Chain Spot: CHAIN_SPOT_CACHE (30-day expiration) + - Dividends: DIVIDEND_CACHE (60-day expiration) + +Data Retrieval Flow: + 1. Check memory cache → return if hit + 2. Check disk cache → populate memory if hit + 3. Query data source (OpenBB/ThetaData) + 4. Process and validate data + 5. Store in all cache levels + 6. Return to caller + +AtIndexResult Structure: + Point-in-time market data snapshot: + - sym: Ticker symbol + - date: Query date (pd.Timestamp) + - spot: OHLCV data (pd.Series) + - chain_spot: Chain-derived spot (pd.Series) + - rates: Risk-free rates (pd.Series) + - dividends: Dividend timeseries (pd.Series) + - additional: Custom data dict + +TimeseriesData Structure: + Complete timeseries for a symbol: + - spot: Full OHLCV DataFrame + - chain_spot: Full chain spot DataFrame + - dividends: Full dividend Series + - additional_data: Dict of custom Series/DataFrames + +MarketTimeseries Features: + Lazy Loading: + - Data loaded on first access + - Avoids memory bloat for unused symbols + - Transparent to caller + + Auto-Refresh: + - Configurable refresh interval (default 30 min) + - Checks last refresh timestamp + - Updates stale data automatically + - Disabled for historical backtests + + Property Protection: + - Direct property access raises UnaccessiblePropertyError + - Forces use of get_timeseries() or get_at_index() + - Prevents inconsistent data states + - Clear error messages guide users + + Signal Handling: + - Registers SIGTERM and SIGINT handlers + - Flushes caches on exit + - Prevents data corruption + - Ensures cleanup in all exit scenarios + +Usage: + # Initialize market timeseries + market_data = MarketTimeseries( + start='2024-01-01', + end='2024-12-31' + ) + + # Get full timeseries for a symbol + ts_data = market_data.get_timeseries( + sym='AAPL', + data_type='spot' + ) + + # Get point-in-time snapshot + snapshot = market_data.get_at_index( + sym='AAPL', + date=pd.Timestamp('2024-06-15') + ) + + # Add custom data + market_data.add_additional_data( + sym='AAPL', + name='custom_indicator', + data=custom_series, + callable_func=lambda df: process(df) + ) + +Integration: + - BacktestTimeseries extends this for backtest-specific needs + - RiskManager uses for all market data access + - OrderPicker queries for chain data + - Position analysis uses for Greek calculations + +Performance Considerations: + - Caching dramatically reduces API calls + - Memory usage grows with symbol count + - Refresh interval trades freshness for performance + - Disk cache speeds up repeated backtests + +Data Sources: + OpenBB: + - Primary source for spot prices + - Dividend data + - Wide symbol coverage + - Free tier available + + ThetaData: + - Option chain data + - Chain-derived spot prices + - High-quality historical data + - Requires subscription + + YFinance (Fallback): + - Backup for spot prices + - Free but rate-limited + - Used when OpenBB fails + +Error Handling: + - YFinanceEmptyData: Raised when no data available + - UnaccessiblePropertyError: Raised on direct property access + - Automatic fallback to alternative sources + - Logging of all data retrieval failures + +Notes: + - All dates handled as pandas Timestamps + - Business day calendar used for date arithmetic + - Data resampled to daily frequency + - Missing data handled via forward-fill + - Thread-safe via proper locking mechanisms +""" + +import numpy as np +from datetime import datetime, timedelta +from dataclasses import dataclass, field +from typing import Any, ClassVar, Dict, List, Literal, Optional +import pandas as pd +from pandas.tseries.offsets import BDay +from dbase.DataAPI.ThetaData import resample # noqa +from trade.helpers.helper import retrieve_timeseries, ny_now, CustomCache, YFinanceEmptyData, to_datetime +from trade.helpers.Logging import setup_logger +from trade.assets.rates import get_risk_free_rate_helper +from EventDriven._vars import OPTION_TIMESERIES_START_DATE, load_riskmanager_cache +from EventDriven.exceptions import UnaccessiblePropertyError +from trade.datamanager.utils.cache import _cache_it_timeseries_data_structure, _data_structure_cache_check_missing +from trade import SIGNALS_TO_RUN + + +logger = setup_logger("trade.datamanager.market_data", stream_log_level="INFO") + +## TODO: This var is from optionlib. Once ready, import from there. +## TODO: Implement interval handling to have multiple intervals + +OPTIMESERIES: Optional["MarketTimeseries"] = None +DIVIDEND_CACHE: CustomCache = load_riskmanager_cache(target="dividend_timeseries") +SPOT_CACHE: CustomCache = load_riskmanager_cache(target="spot_timeseries") +CHAIN_SPOT_CACHE: CustomCache = load_riskmanager_cache(target="chain_spot_timeseries") +SPLIT_FACTOR_CACHE: CustomCache = load_riskmanager_cache( + target="split_factor_timeseries", create_on_missing=True, clear_on_exit=False +) + + +@dataclass +class AtIndexResult: + """Point-in-time market data snapshot for a symbol at a specific date. + + Container for all market data retrieved at a single date/timestamp. Used for + accessing complete market state at a specific point in time for pricing, risk + analysis, or strategy decisions. + + Attributes: + sym: Equity ticker symbol (e.g., "AAPL", "MSFT"). + date: Query date as pd.Timestamp. + spot: OHLCV data series with keys ['open', 'high', 'low', 'close', 'volume']. + chain_spot: Chain-derived spot series (split-adjusted from ThetaData). + rates: Risk-free rate series (currently np.nan, reserved for future use). + dividends: Dividend amount paid on this date (0 if no dividend). + dividend_yield: Calculated yield (dividend / spot close price). + split_factor: Cumulative split adjustment factor (1.0 = no adjustment). + additional: Dictionary of custom/additional data computed for this date. + + Examples: + >>> mts = MarketTimeseries() + >>> result = mts.get_at_index("AAPL", "2025-06-15") + >>> print(f"Close: ${result.spot['close']:.2f}") + >>> print(f"Dividend: ${result.dividends:.2f}") + >>> print(f"Split Factor: {result.split_factor}") + >>> if result.dividends > 0: + ... print(f"Ex-dividend date with yield: {result.dividend_yield:.2%}") + """ + + sym: str + date: pd.Timestamp + spot: pd.Series + chain_spot: pd.Series + rates: pd.Series + dividends: int | float + dividend_yield: int | float + split_factor: float | int + additional: Dict[str, Any] = field(default_factory=dict) + + def __repr__(self) -> str: + return f"AtIndexResult(sym={self.sym}, date={self.date})" + + +@dataclass +class TimeseriesData: + """Complete timeseries data container for a specific symbol. + + Holds all market data types for a symbol as full timeseries (DataFrames or Series). + Returned by MarketTimeseries.get_timeseries() with requested factors populated and + non-requested factors set to None. Used for bulk analysis, backtesting, and + vectorized calculations. + + Attributes: + spot: OHLCV DataFrame with columns ['open', 'high', 'low', 'close', 'volume'] + and DatetimeIndex. None if not requested. + chain_spot: Chain-derived spot DataFrame (split-adjusted) with same structure + as spot plus 'split_factor' column. None if not requested. + dividends: Daily dividend amounts as Series with DatetimeIndex. Values are 0 + on non-dividend dates. None if not requested. + dividend_yield: Calculated yield series (dividends / spot close). None if not + requested or cannot be calculated. + split_factor: Cumulative split adjustment factors as Series with DatetimeIndex. + None if not requested. + rates: Risk-free rate series (annualized). None if not requested. + additional_data: Dictionary mapping custom data names to their Series/DataFrames. + Empty dict if no additional data. + + Examples: + >>> mts = MarketTimeseries() + >>> # Get all data + >>> ts_data = mts.get_timeseries("AAPL") + >>> print(ts_data.spot.head()) + >>> print(f"Total dividends: ${ts_data.dividends.sum():.2f}") + + >>> # Get specific factor only + >>> spot_only = mts.get_timeseries("AAPL", factor="spot") + >>> assert spot_only.dividends is None # Not requested + >>> print(spot_only.spot['close'].mean()) + + >>> # Work with additional custom data + >>> custom = mts.get_timeseries("AAPL", factor="additional", + ... additional_data_name="sma_20") + >>> print(custom.additional_data['sma_20'].tail()) + """ + + spot: pd.DataFrame + chain_spot: pd.DataFrame + dividends: pd.Series + dividend_yield: pd.Series + split_factor: pd.Series + rates: Optional[pd.Series] = None + additional_data: Dict[str, pd.Series] = field(default_factory=dict) + + def __repr__(self) -> str: + return f"TimeseriesData(spot={self.spot is not None}, chain_spot={self.chain_spot is not None}, dividends={self.dividends is not None}, additional_data_keys={list(self.additional_data.keys())})" + + +@dataclass +class MarketTimeseries: + """Comprehensive market data manager with multi-tier caching and lazy loading. + + Central hub for retrieving equity market data (spot prices, dividends, splits, rates) + with intelligent caching at memory and disk levels. Implements lazy loading to minimize + memory footprint and API calls. Prevents direct property access to ensure consistent + data retrieval patterns. + + Architecture: + - Three-tier caching: memory (instant), disk (fast), source (slow) + - Lazy loading: data loaded only when accessed + - Partial cache support: loads missing date ranges incrementally + - Property protection: forces use of get_timeseries() or get_at_index() + - Custom data support: user-defined transformations via callables + + Data Sources: + - Spot prices: OpenBB/YFinance (equity OHLCV) + - Chain spot: ThetaData (option chain underlying prices) + - Dividends: OpenBB (regular and special dividends) + - Rates: Federal Reserve (treasury yield curve) + + Attributes: + additional_data: Dict of custom computed data {name: {symbol: Series}}. + rates: DataFrame of risk-free rates with annualized yields. + DEFAULT_NAMES: Class constant listing standard data types. + _refresh_delta: Time interval for auto-refresh (None = disabled). + _last_refresh: Timestamp of last data refresh. + _start: Default start date for data retrieval (YYYY-MM-DD). + _end: Default end date for data retrieval (YYYY-MM-DD). + _today: Current date string (YYYY-MM-DD). + should_refresh: Enable/disable auto-refresh behavior. + + Protected Properties: + spot, chain_spot, dividends, split_factor: Direct access raises + UnaccessiblePropertyError. Use get_timeseries() or get_at_index() instead. + + Cache Management: + Uses module-level CustomCache instances: + - SPOT_CACHE: 45-day expiration + - CHAIN_SPOT_CACHE: 30-day expiration + - DIVIDEND_CACHE: 60-day expiration + - SPLIT_FACTOR_CACHE: Persistent (no expiration) + + Examples: + >>> # Initialize with custom date range + >>> mts = MarketTimeseries( + ... _start="2025-01-01", + ... _end="2025-12-31" + ... ) + + >>> # Get complete timeseries for a symbol + >>> ts_data = mts.get_timeseries("AAPL") + >>> print(ts_data.spot['close'].mean()) + + >>> # Get point-in-time snapshot + >>> snapshot = mts.get_at_index("AAPL", "2025-06-15") + >>> print(f"Close: ${snapshot.spot['close']:.2f}") + + >>> # Add custom indicator + >>> mts.calculate_additional_data( + ... factor="spot", + ... sym="AAPL", + ... additional_data_name="sma_50", + ... _callable=lambda s: s.rolling(50).mean(), + ... column="close" + ... ) + + >>> # Preload data for multiple symbols + >>> for sym in ["AAPL", "MSFT", "GOOGL"]: + ... mts.load_timeseries(sym) + + >>> # Clear all caches + >>> MarketTimeseries.clear_caches() + + Integration: + Used by: + - RiskManager for all market data access + - BacktestTimeseries for historical simulations + - OrderPicker for option chain data + - Position analysis for Greek calculations + + Thread Safety: + Cache operations are thread-safe via CustomCache locking mechanisms. + Multiple readers can access cached data concurrently. + """ + + additional_data: Dict[str, Any] = field(default_factory=dict) + rates: pd.DataFrame = field(default_factory=get_risk_free_rate_helper) + DEFAULT_NAMES: ClassVar[List[str]] = ["spot", "chain_spot", "dividends", "split_factor", "dividend_yield"] + _refresh_delta: Optional[timedelta] = timedelta(minutes=30) + _last_refresh: Optional[datetime] = field(default_factory=ny_now) + _start: str = OPTION_TIMESERIES_START_DATE + _end: str = (datetime.now() - BDay(1)).strftime("%Y-%m-%d") + _today: str = datetime.now().strftime("%Y-%m-%d") + should_refresh: bool = True + + @property + def spot(self) -> dict: + raise UnaccessiblePropertyError( + "The 'spot' property is not accessible directly. Use 'get_timeseries' method instead. Or access via 'get_at_index' method." + ) + + @property + def split_factor(self) -> dict: + raise UnaccessiblePropertyError( + "The 'split_factor' property is not accessible directly. Use 'get_timeseries' method instead. Or access via 'get_at_index' method." + ) + + @property + def chain_spot(self) -> dict: + raise UnaccessiblePropertyError( + "The 'chain_spot' property is not accessible directly. Use 'get_timeseries' method instead. Or access via 'get_at_index' method." + ) + + @property + def dividends(self) -> dict: + raise UnaccessiblePropertyError( + "The 'dividends' property is not accessible directly. Use 'get_timeseries' method instead. Or access via 'get_at_index' method." + ) + + @property + def _spot(self) -> CustomCache: + return SPOT_CACHE + + @property + def _chain_spot(self) -> CustomCache: + return CHAIN_SPOT_CACHE + + @property + def _dividends(self) -> CustomCache: + return DIVIDEND_CACHE + + @property + def _split_factor(self) -> CustomCache: + return SPLIT_FACTOR_CACHE + + @classmethod + def clear_caches(cls) -> None: + """Clear all caches used by MarketTimeseries. + + Removes all cached data from spot, chain_spot, dividend, and split_factor caches. + Useful for forcing fresh data retrieval or reducing memory usage. + + Examples: + >>> MarketTimeseries.clear_caches() + >>> # All caches cleared, next data access will reload from source + """ + SPOT_CACHE.clear() + CHAIN_SPOT_CACHE.clear() + DIVIDEND_CACHE.clear() + SPLIT_FACTOR_CACHE.clear() + logger.info("All MarketTimeseries caches have been cleared.") + + def _load_spot_into_cache(self, sym: str, start: str, end: str) -> None: + """Load spot OHLCV data for a symbol into the cache. + + Retrieves equity spot prices from data source (OpenBB/YFinance) and stores in + the spot cache with intelligent merge logic for existing data. Handles missing + data gracefully with warning logging. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Start date string (YYYY-MM-DD format). + end: End date string (YYYY-MM-DD format). + + Examples: + >>> mts = MarketTimeseries() + >>> mts._load_spot_into_cache("AAPL", "2025-01-01", "2025-01-31") + >>> # Spot data now cached and available for retrieval + """ + + try: + spot_data = retrieve_timeseries( + tick=sym, + start=start, + end=end, + ) + _cache_it_timeseries_data_structure( + existing=self._spot.get(sym), + key=sym, + value=spot_data, + expire=None, + cache=self._spot, + ) + logger.info("Loaded spot data for symbol %s into cache.", sym) + return spot_data + except YFinanceEmptyData: + logger.warning("No spot data found for symbol %s from data source. Will skip caching.", sym) + return None + + def _load_chain_spot_into_cache(self, sym: str, start: str, end: str) -> None: + """Load chain-derived spot data for a symbol into the cache. + + Retrieves underlying prices from option chain data (ThetaData) and stores in + the chain_spot cache. Chain spot is split-adjusted and may differ from equity + spot due to timing. Used for consistent option pricing. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Start date string (YYYY-MM-DD format). + end: End date string (YYYY-MM-DD format). + + Examples: + >>> mts = MarketTimeseries() + >>> mts._load_chain_spot_into_cache("AAPL", "2025-01-01", "2025-01-31") + >>> # Chain spot data now cached with split adjustments + """ + try: + chain_spot_data = retrieve_timeseries( + tick=sym, + start=start, + end=end, + spot_type="chain_spot", + ) + _cache_it_timeseries_data_structure( + existing=self._chain_spot.get(sym), + key=sym, + value=chain_spot_data, + expire=None, + cache=self._chain_spot, + ) + logger.info("Loaded chain spot data for symbol %s into cache.", sym) + return chain_spot_data + except YFinanceEmptyData: + logger.warning("No chain spot data found for symbol %s from data source. Will skip caching.", sym) + return None + + def _load_dividends_into_cache(self, sym: str, start: str = None, end: str = None) -> None: + """Load daily dividend timeseries for a symbol into the cache. + + Retrieves regular and special dividends with ex-dates from data source and stores + in the dividends cache. Used for American option pricing and forward calculations. + Defaults to instance start/end dates if not provided. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Optional start date string (YYYY-MM-DD). Defaults to self._start. + end: Optional end date string (YYYY-MM-DD). Defaults to self._end. + + Examples: + >>> mts = MarketTimeseries() + >>> mts._load_dividends_into_cache("AAPL") + >>> # Loads dividends for instance's full date range + >>> mts._load_dividends_into_cache("MSFT", "2025-01-01", "2025-06-30") + >>> # Loads dividends for specific date range + """ + from trade.datamanager.market_data_helpers.dividends import get_daily_dividends_timeseries + + try: + + divs = get_daily_dividends_timeseries(sym, start=start or self._start, end=end or self._end) + _cache_it_timeseries_data_structure( + existing=self._dividends.get(sym), + key=sym, + value=divs, + expire=None, + cache=self._dividends, + skip_today_check=True, # Dividends don't change intraday, so skip today check + ) + logger.info("Loaded dividend data for symbol %s into cache.", sym) + return divs + except YFinanceEmptyData: + logger.warning("No dividend data found for symbol %s from data source. Will skip caching.", sym) + + def _load_split_factor_into_cache(self, sym: str, start: str, *args, **kwargs) -> None: + """Load split factor timeseries for a symbol into the cache. + + Extracts split factors from chain spot data and stores in the split_factor cache. + Split factors are cumulative multipliers for historical price adjustment. Skips + today check since splits don't change intraday. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Start date string (YYYY-MM-DD format). End date uses instance _end. + + Examples: + >>> mts = MarketTimeseries() + >>> mts._load_split_factor_into_cache("AAPL", "2025-01-01") + >>> # Split factors loaded from chain spot data + """ + try: + self._load_chain_spot_into_cache(sym, start, self._end) + chain_spot = self._chain_spot.get(sym) + split_factor = chain_spot["split_factor"] + _cache_it_timeseries_data_structure( + existing=self._split_factor.get(sym), + key=sym, + value=split_factor, + expire=None, + cache=self._split_factor, + ## Cutting out today check as split factors don't change intraday + skip_today_check=True, + ) + logger.info("Loaded split factor data for symbol %s into cache.", sym) + except YFinanceEmptyData: + logger.warning("No split factor data found for symbol %s from data source. Will skip caching.", sym) + + def _clip_to_date_range(self, df: pd.DataFrame | pd.Series, start: str, end: str, *args, **kwargs) -> pd.DataFrame | pd.Series: + """Clip a DataFrame or Series to the specified date range. + + Filters timeseries data to only include dates within [start, end] inclusive. + Uses date objects for comparison to handle datetime vs date mismatches. + + Args: + df: DataFrame or Series with DatetimeIndex to filter. + start: Start date string (YYYY-MM-DD format). + end: End date string (YYYY-MM-DD format). + + Returns: + Filtered DataFrame or Series with only dates in range. + + Examples: + >>> mts = MarketTimeseries() + >>> spot_full = mts._get_spot_timeseries("AAPL") + >>> spot_q1 = mts._clip_to_date_range(spot_full, "2025-01-01", "2025-03-31") + >>> # Returns only Q1 2025 data + """ + clipped = df[(df.index.date >= to_datetime(start).date()) & (df.index.date <= to_datetime(end).date())] + return clipped + + def _get_spot_timeseries(self, sym: str, start: str = None, end: str = None, *args, **kwargs) -> pd.DataFrame: + """Retrieve spot OHLCV timeseries for a symbol with automatic cache management. + + Checks cache for existing data, loads from source if missing, and handles partial + cache hits by loading only missing dates. Automatically clips to requested range. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Optional start date string (YYYY-MM-DD). Defaults to self._start. + end: Optional end date string (YYYY-MM-DD). Defaults to self._end. + **kwargs: Additional arguments (currently unused, for extensibility). + + Returns: + DataFrame with OHLCV columns (open, high, low, close, volume) and DatetimeIndex. + + Examples: + >>> mts = MarketTimeseries() + >>> spot = mts._get_spot_timeseries("AAPL", "2025-01-01", "2025-01-31") + >>> print(spot.columns) # ['open', 'high', 'low', 'close', 'volume'] + """ + start = start or self._start + end = end or self._end + cached_data = self._spot.get(sym) + if cached_data is None: + cached_data = self._load_spot_into_cache(sym, start, end) + + cached_data, is_partial, missing_start_date, missing_end_date = _data_structure_cache_check_missing( + cached_data=cached_data, + key=sym, + start_dt=start, + end_dt=end, + ) + if is_partial: + data = self._load_spot_into_cache( + sym, missing_start_date.strftime("%Y-%m-%d"), missing_end_date.strftime("%Y-%m-%d") + ) + cached_data = pd.concat([cached_data, data]).sort_index() + + return self._clip_to_date_range(cached_data, start, end) + + def _get_chain_spot_timeseries(self, sym: str, start: str = None, end: str = None, *args, **kwargs) -> pd.DataFrame: + """Retrieve chain-derived spot timeseries for a symbol with automatic cache management. + + Checks cache for existing chain spot data, loads from ThetaData if missing, and + handles partial cache hits. Chain spot is split-adjusted and used for option pricing. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Optional start date string (YYYY-MM-DD). Defaults to self._start. + end: Optional end date string (YYYY-MM-DD). Defaults to self._end. + **kwargs: Additional arguments (currently unused, for extensibility). + + Returns: + DataFrame with chain spot columns including split_factor and DatetimeIndex. + + Examples: + >>> mts = MarketTimeseries() + >>> chain_spot = mts._get_chain_spot_timeseries("AAPL", "2025-01-01", "2025-01-31") + >>> print(chain_spot['split_factor']) # Cumulative split adjustments + """ + + start = start or self._start + end = end or self._end + cached_data = self._chain_spot.get(sym) + if cached_data is None: + cached_data = self._load_chain_spot_into_cache(sym, start, end) + cached_data, is_partial, missing_start_date, missing_end_date = _data_structure_cache_check_missing( + cached_data=cached_data, + key=sym, + start_dt=start, + end_dt=end, + ) + if is_partial: + data = self._load_chain_spot_into_cache( + sym, missing_start_date.strftime("%Y-%m-%d"), missing_end_date.strftime("%Y-%m-%d") + ) + cached_data = pd.concat([cached_data, data]).sort_index() + + return self._clip_to_date_range(cached_data, start, end) + + def _get_dividends_timeseries(self, sym: str, start: str = None, end: str = None, *args, **kwargs) -> pd.Series: + """Retrieve daily dividend timeseries for a symbol with automatic cache management. + + Checks cache for existing dividend data, loads from source if missing, and handles + partial cache hits. Returns daily dividend amounts with ex-dates as index. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Optional start date string (YYYY-MM-DD). Defaults to self._start. + end: Optional end date string (YYYY-MM-DD). Defaults to self._end. + **kwargs: Additional arguments (currently unused, for extensibility). + + Returns: + Series with dividend amounts and DatetimeIndex of ex-dates. + + Examples: + >>> mts = MarketTimeseries() + >>> divs = mts._get_dividends_timeseries("AAPL", "2025-01-01", "2025-12-31") + >>> print(divs[divs > 0]) # Show only dividend payment dates + """ + if start is None: + start = self._start + if end is None: + end = self._end + cached_data = self._dividends.get(sym) + if cached_data is None: + cached_data = self._load_dividends_into_cache(sym, start, end) + cached_data, is_partial, missing_start_date, missing_end_date = _data_structure_cache_check_missing( + cached_data=cached_data, + key=sym, + start_dt=start, + end_dt=end, + ) + if is_partial: + data = self._load_dividends_into_cache( + sym, missing_start_date.strftime("%Y-%m-%d"), missing_end_date.strftime("%Y-%m-%d") + ) + cached_data = pd.concat([cached_data, data]).sort_index() + + return self._clip_to_date_range(cached_data, start, end, *args, **kwargs) + + def _get_split_factor_timeseries(self, sym: str, start: str = None, end: str = None, *args, **kwargs) -> pd.Series: + """Retrieve split factor timeseries for a symbol with automatic cache management. + + Checks cache for existing split factor data, extracts from chain spot if missing, + and handles partial cache hits. Split factors are cumulative multipliers for + historical price adjustment. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start: Optional start date string (YYYY-MM-DD). Defaults to self._start. + end: Optional end date string (YYYY-MM-DD). Defaults to self._end. + **kwargs: Additional arguments (currently unused, for extensibility). + + Returns: + Series with cumulative split factors and DatetimeIndex. + + Examples: + >>> mts = MarketTimeseries() + >>> splits = mts._get_split_factor_timeseries("AAPL", "2025-01-01", "2025-12-31") + >>> print(splits[splits != 1.0]) # Show dates with splits + """ + start = start or self._start + end = end or self._end + cached_data = self._split_factor.get(sym) + if cached_data is None: + self._load_split_factor_into_cache(sym, start) + cached_data = self._split_factor.get(sym) + cached_data, is_partial, missing_start_date, missing_end_date = _data_structure_cache_check_missing( + cached_data=cached_data, + key=sym, + start_dt=start, + end_dt=self._end, + ) + if is_partial: + self._load_split_factor_into_cache(sym, missing_start_date.strftime("%Y-%m-%d")) + cached_data = self._split_factor.get(sym) + + return self._clip_to_date_range(cached_data, start, end, *args, **kwargs) + + def _get_dividend_yield_timeseries(self, sym: str, *args, **kwargs) -> pd.Series: + """Calculate and retrieve dividend yield timeseries for a symbol. + + Computes daily dividend yield by dividing dividend amounts by spot close prices. + Automatically retrieves spot and dividend data from cache or loads if needed. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + **kwargs: Additional arguments (currently unused, for extensibility). + + Returns: + Series with dividend yields (as decimals, not percentages) and DatetimeIndex. + + Examples: + >>> mts = MarketTimeseries() + >>> yield_ts = mts._get_dividend_yield_timeseries("AAPL") + >>> print(yield_ts.mean() * 100) # Average yield as percentage + """ + spot = self._get_spot_timeseries(sym) + dividends = self._get_dividends_timeseries(sym) + dividend_yield = dividends / spot["close"] + # Fill non-dividend dates with 0 yield. I believe it should be fine + # TODO: Pay close attention to this. Maybe find an alternative way to handle non-dividend dates if it causes issues. + dividend_yield.fillna(0.0, inplace=True) + + return self._clip_to_date_range(dividend_yield, self._start, self._end, *args, **kwargs) + + def get_split_factor_at_index(self, sym: str, index: pd.Timestamp, *args, **kwargs) -> float | int: + """Retrieve the split factor for a symbol at a specific date with forward-fill logic. + + Returns the cumulative split factor at the requested date. If the exact date is not + in the series, returns the most recent prior split factor (forward-fill). Returns + 1.0 if no data exists or the date precedes all split data. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + index: Date for split factor lookup (pd.Timestamp or date string). + + Returns: + Cumulative split factor at the specified date (1.0 = no adjustment). + + Examples: + >>> mts = MarketTimeseries() + >>> factor = mts.get_split_factor_at_index("AAPL", "2025-06-15") + >>> print(f"Split factor: {factor}") + >>> # Returns 1.0 if no splits, or adjustment factor if splits occurred + """ + split_factor_series = self._split_factor.get(sym) + if split_factor_series is None: + return 1.0 + + index = pd.to_datetime(index) + if index in split_factor_series.index: + return split_factor_series.loc[index] + else: + prior_dates = split_factor_series.index[split_factor_series.index <= index] + if not prior_dates.empty: + nearest_date = prior_dates.max() + return split_factor_series.loc[nearest_date] + else: + return 1.0 + + def get_at_index(self, sym: str, index: pd.Timestamp, *args, **kwargs) -> AtIndexResult: + """Retrieve point-in-time market data snapshot for a symbol at a specific date. + + Returns a complete snapshot of market data (spot, chain_spot, dividends, rates, + split_factor, dividend_yield) for a single date. Ensures all necessary data is + loaded before retrieval. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + index: Date for data snapshot (pd.Timestamp or date string YYYY-MM-DD). + interval: Time interval (currently only "1d" supported). + + Returns: + AtIndexResult containing spot (Series), chain_spot (Series), dividends (float), + rates (float), dividend_yield (float), split_factor (float), and metadata. + + Examples: + >>> mts = MarketTimeseries() + >>> snapshot = mts.get_at_index("AAPL", "2025-06-15") + >>> print(snapshot.spot['close']) # Closing price + >>> print(snapshot.dividends) # Dividend amount on this date + >>> print(snapshot.split_factor) # Cumulative split adjustment + """ + + ## Ensure data is loaded for the symbol + sym = sym.upper() + index = to_datetime(index, format="%Y-%m-%d") + spot = self._get_spot_timeseries(sym, start=index, end=index) + chain_spot = self._get_chain_spot_timeseries(sym, start=index, end=index) + dividends = self._get_dividends_timeseries(sym, start=index, end=index) + split_factor = self._get_split_factor_timeseries(sym, start=index) + + ## Retrieve data at index + index_str = index.strftime("%Y-%m-%d") + spot = spot.loc[index_str] if index_str in spot.index else None + chain_spot = chain_spot.loc[index_str] if index_str in chain_spot.index else None + dividends = dividends.loc[index_str] if index_str in dividends.index else 0.0 + rates = np.nan + dividend_yield = dividends / spot["close"] if spot is not None and dividends is not None else None + split_factor = split_factor.loc[index_str] if index_str in split_factor.index else 1.0 + + return AtIndexResult( + spot=spot, + chain_spot=chain_spot, + dividends=dividends, + sym=sym, + date=index_str, + rates=rates, + dividend_yield=dividend_yield, + split_factor=split_factor, + ) + + def calculate_additional_data( + self, + factor: Literal["spot", "chain_spot", "dividends", "split_factor"], + sym: str, + additional_data_name: str, + _callable: Any, + column: Optional[str] = "close", + force_add: bool = False, + *args, + **kwargs, + ) -> None: + """Load additional data for a factor using a custom transformation function. + + Applies a user-defined callable to existing market data to create custom indicators + or derived timeseries. The callable receives a pd.Series and must return a pd.Series. + Results are stored in the additional_data dictionary for later retrieval. + + Storage Schema: + additional_data = {additional_data_name: {sym: pd.Series}} + + Args: + factor: Base data type to transform ('spot', 'chain_spot', 'dividends', 'split_factor'). + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + additional_data_name: Identifier for storing the computed data. + _callable: Function that takes pd.Series and returns pd.Series. + column: Column to extract from DataFrame factors (e.g., 'close', 'volume'). + force_add: If True, overwrites existing data for this name and symbol. + + Raises: + ValueError: If factor not recognized. + ValueError: If symbol data not found for the specified factor. + ValueError: If column not found in factor DataFrame. + + Examples: + >>> mts = MarketTimeseries() + >>> # Calculate 20-day moving average of close prices + >>> mts.calculate_additional_data( + ... factor="spot", + ... sym="AAPL", + ... additional_data_name="sma_20", + ... _callable=lambda s: s.rolling(20).mean(), + ... column="close" + ... ) + >>> # Calculate RSI from dividends + >>> mts.calculate_additional_data( + ... factor="dividends", + ... sym="MSFT", + ... additional_data_name="div_rsi", + ... _callable=lambda s: compute_rsi(s, period=14) + ... ) + """ + + ## Raise error if factor not recognized + if factor not in self.DEFAULT_NAMES: + raise ValueError(f"Factor {factor} not recognized. Must be one of ['spot', 'chain_spot', 'dividends'].") + + ## Get the data for the specified factor and symbol + factor_data = getattr(self, factor).get(sym) + + ## Raise error if symbol not found + if factor_data is None: + raise ValueError(f"No data found for factor {factor} and symbol {sym}.") + + ## If column specified, ensure it exists in the DataFrame + if column and isinstance(factor_data, (pd.DataFrame, pd.Series)): + if column not in factor_data.columns: + raise ValueError(f"Column {column} not found in data for factor {factor} and symbol {sym}.") + factor_data = factor_data[column] + + ## Process the data using the provided callable + processed_data = _callable(factor_data) + if additional_data_name not in self.additional_data: + self.additional_data[additional_data_name] = {} + + ## Check if data already exists and force_add is not set + exists = sym in self.additional_data.get(additional_data_name, {}) + if exists and not force_add: + logger.info( + "Additional data for %s and symbol %s already exists. Use force_add=True to overwrite.", + additional_data_name, + sym, + ) + return + + self.additional_data[additional_data_name][sym] = processed_data + + def get_timeseries( + self, + sym: str, + factor: Literal["spot", "chain_spot", "dividends", "split_factor", "additional"] = None, + additional_data_name: Optional[str] = None, + start_date: str | datetime = None, + end_date: str | datetime = None, + *args, + **kwargs, + ) -> TimeseriesData: + """Retrieve timeseries data for a symbol with optional factor and date filtering. + + Main method for accessing market data. Can return specific factors (spot, chain_spot, + dividends, split_factor), additional custom data, or all factors combined. Automatically + handles caching, data loading, and date range filtering. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + factor: Data type to retrieve. If None, returns all factors. + additional_data_name: Required when factor='additional'. Identifies custom data. + start_date: Optional start date for filtering (YYYY-MM-DD string or datetime). + end_date: Optional end date for filtering (YYYY-MM-DD string or datetime). + + Returns: + TimeseriesData containing requested data. Non-requested fields are None. + + Raises: + ValueError: If factor not recognized. + ValueError: If factor='additional' but additional_data_name not provided. + ValueError: If additional_data_name not found in cached additional data. + ValueError: If no data found for requested factor and symbol. + + Examples: + >>> mts = MarketTimeseries() + >>> # Get all data for a symbol + >>> all_data = mts.get_timeseries("AAPL") + >>> print(all_data.spot.head()) + >>> print(all_data.dividends.sum()) + + >>> # Get specific factor with date range + >>> spot_q1 = mts.get_timeseries( + ... "AAPL", + ... factor="spot", + ... start_date="2025-01-01", + ... end_date="2025-03-31" + ... ) + + >>> # Get custom additional data + >>> sma_data = mts.get_timeseries( + ... "AAPL", + ... factor="additional", + ... additional_data_name="sma_20" + ... ) + + >>> # Calculate dividend yield on the fly + >>> yield_data = mts.get_timeseries("MSFT", factor="dividend_yield") + """ + + data_funcs = { + "spot": self._get_spot_timeseries, + "chain_spot": self._get_chain_spot_timeseries, + "dividends": self._get_dividends_timeseries, + "split_factor": self._get_split_factor_timeseries, + "dividend_yield": self._get_dividend_yield_timeseries, + } + sym = sym.upper() + end_date = end_date or self._end + + if factor not in self.DEFAULT_NAMES + ["additional", None]: + raise ValueError(f"Factor {factor} not recognized. Must be one of {self.DEFAULT_NAMES + ['additional']}.") + if factor == "additional": + if additional_data_name is None: + raise ValueError("additional_data_name must be provided when factor is 'additional'.") + data = self.additional_data.get(additional_data_name, {}).get(sym) + if data is None: + raise ValueError(f"No additional data found for name {additional_data_name} and symbol {sym}.") + return TimeseriesData( + spot=None, + chain_spot=None, + dividends=None, + additional_data={additional_data_name: data}, + split_factor=None, + dividend_yield=None, + ) + + elif factor in self.DEFAULT_NAMES: + ## Retrieve data for the specified factor + data = None + if factor in ["spot", "chain_spot", "dividends", "split_factor"]: + data = data_funcs[factor](sym, start=start_date, end=end_date) + + ## Special handling for dividend_yield + elif factor == "dividend_yield": + divs = self._get_dividends_timeseries(sym, start=start_date, end=end_date) + spot = self._get_spot_timeseries(sym, start=start_date, end=end_date) + + ## Ensure we have both dividends and spot data + if divs is None: + raise ValueError(f"No dividend data found for symbol {sym} to calculate dividend yield.") + if spot is None: + raise ValueError(f"No spot data found for symbol {sym} to calculate dividend yield.") + + ## Calculate dividend yield + dividend_yield = divs / spot["close"] + data = dividend_yield + + ## Filter data by start_date and end_date if provided + if start_date is not None or end_date is not None: + start_date = pd.to_datetime(start_date).strftime("%Y-%m-%d") if start_date is not None else None + end_date = pd.to_datetime(end_date).strftime("%Y-%m-%d") if end_date is not None else None + + if start_date is not None: + data = data[data.index >= start_date] + if end_date is not None: + data = data[data.index <= end_date] + + ## Construct TimeseriesData based on the factor + if data is None: + raise ValueError(f"No data found for factor {factor} and symbol {sym}.") + if factor == "spot": + ts = TimeseriesData(spot=data, chain_spot=None, dividends=None, dividend_yield=None, split_factor=None) + elif factor == "chain_spot": + ts = TimeseriesData(spot=None, chain_spot=data, dividends=None, dividend_yield=None, split_factor=None) + elif factor == "dividends": + ts = TimeseriesData(spot=None, chain_spot=None, dividends=data, dividend_yield=None, split_factor=None) + elif factor == "dividend_yield": + ts = TimeseriesData(spot=None, chain_spot=None, dividends=None, dividend_yield=data, split_factor=None) + elif factor == "split_factor": + ts = TimeseriesData(spot=None, chain_spot=None, dividends=None, split_factor=data, dividend_yield=None) + else: + raise ValueError(f"Unhandled factor {factor}.") + + ## If no factor specified, return all data + elif factor is None: + spot = self._get_spot_timeseries(sym, start=start_date, end=end_date) + chain_spot = self._get_chain_spot_timeseries(sym, start=start_date, end=end_date) + dividends = self._get_dividends_timeseries(sym, start=start_date, end=end_date) + dividend_yield = dividends / spot["close"] if spot is not None and dividends is not None else None + split_factor = self._get_split_factor_timeseries(sym, start=start_date, end=end_date) + + ## Filter data by start_date and end_date if provided + if start_date is not None or end_date is not None: + start_date = pd.to_datetime(start_date).strftime("%Y-%m-%d") if start_date is not None else None + end_date = pd.to_datetime(end_date).strftime("%Y-%m-%d") if end_date is not None else None + + ## Start date filter + if start_date is not None: + spot = spot[spot.index >= start_date] + chain_spot = chain_spot[chain_spot.index >= start_date] + dividends = dividends[dividends.index >= start_date] + dividend_yield = dividend_yield[dividend_yield.index >= start_date] + split_factor = split_factor[split_factor.index >= start_date] + + ## End date filter + if end_date is not None: + spot = spot[spot.index <= end_date] + chain_spot = chain_spot[chain_spot.index <= end_date] + dividends = dividends[dividends.index <= end_date] + dividend_yield = dividend_yield[dividend_yield.index <= end_date] + split_factor = split_factor[split_factor.index <= end_date] + + ## Construct TimeseriesData with all data + ts = TimeseriesData( + spot=spot, + chain_spot=chain_spot, + dividends=dividends, + dividend_yield=dividend_yield, + split_factor=split_factor, + rates=self.rates["annualized"], + ) + + return ts + + def load_timeseries(self, sym: str, start_date: str = None, end_date: str = None, *args, **kwargs) -> None: + """Preload all market data timeseries for a symbol into cache. + + Eagerly loads spot, chain_spot, dividends, and split_factor data into their + respective caches. Useful for warming cache before intensive operations or + reducing latency for first access. Uses instance date range if not specified. + + Args: + sym: Stock ticker symbol (e.g., "AAPL", "MSFT"). + start_date: Optional start date string (YYYY-MM-DD). Defaults to self._start. + end_date: Optional end date string (YYYY-MM-DD). Defaults to self._end. + + Examples: + >>> mts = MarketTimeseries() + >>> # Preload full date range for a symbol + >>> mts.load_timeseries("AAPL") + >>> # Now all subsequent access for AAPL will be instant + + >>> # Preload specific date range + >>> mts.load_timeseries("MSFT", "2025-01-01", "2025-12-31") + + >>> # Batch preload multiple symbols + >>> for sym in ["AAPL", "MSFT", "GOOGL"]: + ... mts.load_timeseries(sym) + """ + sym = sym.upper() + start_date = start_date or self._start + end_date = end_date or self._end + self._load_spot_into_cache(sym, start_date, end_date) + self._load_chain_spot_into_cache(sym, start_date, end_date) + self._load_dividends_into_cache(sym, start_date, end_date) + self._load_split_factor_into_cache(sym, start_date, end_date) + + def __repr__(self) -> str: + return f"MarketTimeseries(symbols: {list(self._spot.keys())}, intervals: {list(self._spot.keys())})" + + +def get_timeseries_obj(live: bool = False, *args, **kwargs) -> MarketTimeseries: + """Get or create the singleton MarketTimeseries instance. + + Returns the global OPTIMESERIES instance, creating it if necessary. Implements + singleton pattern to ensure only one MarketTimeseries exists per session, sharing + caches across all callers for optimal performance. + + Args: + live: If True, sets end date to today. If False, sets to last business day. + + Returns: + Global MarketTimeseries singleton instance. + + Examples: + >>> # Get singleton instance for backtesting (end = yesterday) + >>> mts = get_timeseries_obj(live=False) + >>> data = mts.get_timeseries("AAPL") + + >>> # Get singleton for live trading (end = today) + >>> mts_live = get_timeseries_obj(live=True) + >>> # Same instance if called again + >>> assert get_timeseries_obj() is mts_live + """ + global OPTIMESERIES + if OPTIMESERIES is None: + OPTIMESERIES = MarketTimeseries( + _end=(datetime.now() - BDay(1)).strftime("%Y-%m-%d") if not live else datetime.now().strftime("%Y-%m-%d") + ) + + return OPTIMESERIES + + +def reset_timeseries_obj(*args, **kwargs) -> None: + """Reset the singleton MarketTimeseries instance to None. + + Clears the global OPTIMESERIES variable, forcing the next call to + get_timeseries_obj() to create a fresh instance. Useful for testing or + when switching between live and backtest modes. Does not clear caches. + + Examples: + >>> mts = get_timeseries_obj(live=False) + >>> # ... use mts ... + >>> reset_timeseries_obj() # Clear singleton + >>> mts_live = get_timeseries_obj(live=True) # New instance + >>> assert mts is not mts_live + """ + global OPTIMESERIES + OPTIMESERIES = None + + +if __name__ == "__main__": + mts = get_timeseries_obj() + mts.load_timeseries("BA", force=True) + ts = mts.get_timeseries("BA") + print(ts) + print(SIGNALS_TO_RUN) diff --git a/trade/datamanager/market_data_helpers/dividends.py b/trade/datamanager/market_data_helpers/dividends.py new file mode 100644 index 0000000..a8d1584 --- /dev/null +++ b/trade/datamanager/market_data_helpers/dividends.py @@ -0,0 +1,131 @@ +from datetime import datetime, timedelta +from openbb import obb +import pandas as pd +from trade.optionlib.config.defaults import ( + OPTION_TIMESERIES_START_DATE, # noqa +) +from trade.helpers.Logging import setup_logger +from trade.helpers.helper import CustomCache +from dataclasses import dataclass +from trade.datamanager.vars import DM_GEN_PATH +from trade.optionlib.assets.dividend import infer_frequency, FREQ_MAP +from trade.datamanager.utils.logging import get_logging_level, register_to_factor_list +from trade.datamanager.config import OptionDataConfig +logger = setup_logger("trade.datamanager.market_data_helpers.dividends", stream_log_level=get_logging_level()) +register_to_factor_list("trade.datamanager.market_data_helpers.dividends") + + +@dataclass +class SavedDividendsResult: + symbol: str + historicals: pd.Series + resampled_timeseries: pd.Series + last_updated: datetime + + +## Cache has to be in memory. Incase dividends update on another date +DIVIDEND_CACHE = CustomCache( + location=DM_GEN_PATH, fname="discrete_dividends_timeseries", clear_on_exit=False, expire_days=365 +) +def resample_dividends_to_daily(div_series: pd.Series, buffer: int = 30) -> pd.Series: + """Resample dividend series to daily frequency with forward fill.""" + + freq = infer_frequency(div_series) + if freq is None: + raise ValueError("Could not infer frequency.") + freq_days = FREQ_MAP[freq] * 30 # Approximate to days + freq_days += buffer + + ## First, resample to 1b (daily business days) + resampled = div_series.resample("1b").ffill() + + ## Next, the resampled is clearly missing last dividends to today or end_date, + ## SO we will forward fill the last known dividend to today. But with some rules. + ## There are cases where dividends were discontinued, so we will only forward fill if the last known dividend date - today is less than freq_days + ## If not we fill with zeros + last_div_date = div_series.dropna().index[-1] + today = datetime.now() + days_since_last_div = (today - last_div_date).days + + ## Add additional days to ffill into + resampled = resampled.reindex(pd.date_range(start=resampled.index[0], end=today, freq="1b")) + if days_since_last_div <= freq_days: + resampled = resampled.ffill() + else: + logger.info("Filling with zeros as dividends seem to be discontinued.") + resampled.loc[last_div_date + timedelta(days=1) : today] = 0.0 + resampled.index = pd.to_datetime(resampled.index, format="%Y-%m-%d") + resampled.name = "dividend_amount" + resampled.index.name = "datetime" + resampled.sort_index(inplace=True) + return resampled + +def get_div_schedule(ticker: str): + """ + Fetch the dividend schedule for a given ticker. + If the ticker is not in the cache, it fetches the data from yfinance and caches it. + If the ticker is in the cache, it retrieves the data from the cache. + If filter_specials is True, it filters out dividends >= 7.5. + Returns a DataFrame with the dividend schedule. + """ + + ## We're going to use a multi-level dividend retrieval. CustomCache is on disk cache + ## 1. We first check if the symbol is in the on disk DIVIDEND_CACHE + ## 2. If not, we fetch from yfinance via openbb and store in DIVIDEND_CACHE and save with last_updated + ## 3. If in cache, we retrieve from cache, but still check last_updated. + ## We will use a weekly update policy to refresh dividends + ## 4. Return the dividend schedule DataFrame + + # Check if ticker is in cache + filter_specials = OptionDataConfig().filter_out_special_dividends + key = (ticker, filter_specials) + if key not in DIVIDEND_CACHE: + try: + div_history = obb.equity.fundamental.dividends(symbol=ticker, provider="yfinance").to_df() + div_history.set_index("ex_dividend_date", inplace=True) + div_history["amount"] = div_history["amount"].astype(float) + div_history.index = pd.to_datetime(div_history.index) + dividends_data = SavedDividendsResult( + symbol=ticker, + historicals=div_history["amount"], + resampled_timeseries=None, + last_updated=datetime.now(), + ) + except Exception as e: # noqa + div_history = pd.DataFrame( + {"amount": [0]}, index=pd.bdate_range(start="2001-01-01", end=datetime.now(), freq="1Q") + ) + dividends_data = SavedDividendsResult( + symbol=ticker, + historicals=div_history["amount"], + resampled_timeseries=None, + last_updated=datetime.now(), + ) + DIVIDEND_CACHE[key] = dividends_data + + else: + logger.info(f"Ticker {ticker} found in dividend cache.") + dividends_data: SavedDividendsResult = DIVIDEND_CACHE[key] + # Check if we need to refresh (if last_updated > 7 days) + if (datetime.now() - dividends_data.last_updated).days > 7: + del DIVIDEND_CACHE[key] + return get_div_schedule(ticker) + + # Filter out dividends >= 7.5 + if filter_specials: + dividends_data.historicals = dividends_data.historicals[dividends_data.historicals < 7.5] + + return dividends_data.historicals.sort_index() + +def get_daily_dividends_timeseries(ticker, start, end): + """ + Get daily resampled dividend timeseries for a given ticker between start and end dates. + This function retrieves the dividend schedule, resamples it to daily frequency, and filters it to the specified date range. + Returns a pd.Series with daily dividend amounts. + """ + + # Else we fallthrough to refetching the schedule + div_series = get_div_schedule(ticker) + daily_div_series = resample_dividends_to_daily(div_series) + daily_div_series = daily_div_series[start:end] + return daily_div_series diff --git a/trade/datamanager/notebooks/create.ipynb b/trade/datamanager/notebooks/create.ipynb index e1b5b7b..a17abb6 100644 --- a/trade/datamanager/notebooks/create.ipynb +++ b/trade/datamanager/notebooks/create.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 217, + "execution_count": 42, "id": "79ddd501", "metadata": {}, "outputs": [ @@ -11,7 +11,9 @@ "output_type": "stream", "text": [ "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" + " %reload_ext autoreload\n", + "2026-01-18 19:43:31 [test] trade.__init__ INFO: Signal function for `_on_exit_sanitize` added to signal number 15.\n", + "2026-01-18 19:43:31 [test] trade.__init__ INFO: Exit handler `_on_exit_sanitize` registered for normal program exit.\n" ] } ], @@ -32,352 +34,76 @@ " get_vectorized_dividend_rate,\n", " get_div_histories,\n", " _dual_project_dividends,\n", - " ScheduleEntry\n", + " ScheduleEntry,\n", + " Schedule,\n", + " SECONDS_IN_YEAR,\n", + " SECONDS_IN_DAY,\n", ")\n", "import os\n", - "from EventDriven.riskmanager.market_data import MarketTimeseries\n", + "from trade import HOLIDAY_SET\n", + "from trade.helpers.helper import is_USholiday, is_busday, to_datetime\n", + "from EventDriven.riskmanager.market_data import MarketTimeseries, AtIndexResult, TimeseriesData\n", "import pandas as pd\n", - "from typing import Optional, Dict, Union, List\n", + "from typing import Optional, Dict, Union, List, Iterable\n", "from typing import overload, Literal\n", - "DM_GEN_PATH = Path(os.getenv(\"GEN_CACHE_PATH\")) / \"dm_gen_cache\"\n", - "TS = MarketTimeseries()" - ] - }, - { - "cell_type": "code", - "execution_count": 213, - "id": "ec207715", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "2016-12-30 NaN\n", - "2017-01-02 NaN\n", - "2017-01-03 0.005323\n", - "2017-01-04 0.005329\n", - "2017-01-05 0.005302\n", - " ... \n", - "2025-12-31 0.000956\n", - "2026-01-01 NaN\n", - "2026-01-02 0.000959\n", - "2026-01-05 0.000973\n", - "2026-01-06 0.000991\n", - "Length: 2353, dtype: float64" - ] - }, - "execution_count": 213, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 215, - "id": "ff5684eb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'sym': 'AAPL',\n", - " 'date': '2023-01-03',\n", - " 'spot': open 128.34378\n", - " high 128.954561\n", - " low 122.324586\n", - " close 123.211212\n", - " volume 112117500\n", - " chain_price 123.211212\n", - " unadjusted_close 492.844849\n", - " split_ratio 1.0\n", - " cum_split 4.0\n", - " split_factor 1.0\n", - " max_cum_split 4.0\n", - " is_split_date False\n", - " Name: 2023-01-03 00:00:00, dtype: object,\n", - " 'chain_spot': open 128.34378\n", - " high 128.954561\n", - " low 122.324586\n", - " close 123.211212\n", - " volume 112117500\n", - " chain_price 123.211212\n", - " unadjusted_close 27599.311523\n", - " split_ratio 1.0\n", - " cum_split 224.0\n", - " split_factor 1.0\n", - " max_cum_split 224.0\n", - " is_split_date False\n", - " cum_split_from_start 4.0\n", - " Name: 2023-01-03 00:00:00, dtype: object,\n", - " 'rates': daily 0.004559\n", - " annualized 0.0426\n", - " name ^IRX\n", - " description 13 WEEK TREASURY BILL\n", - " Name: 2023-01-03 00:00:00, dtype: object,\n", - " 'dividends': np.float64(0.23),\n", - " 'dividend_yield': np.float64(0.0018667132314604627),\n", - " 'additional': {}}" - ] - }, - "execution_count": 215, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "TS.get_at_index(\"AAPL\", \"2023-01-03\").__dict__" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "id": "2107f515", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'aapl': amount\n", - " ex_dividend_date \n", - " 1987-05-11 0.000536\n", - " 1987-08-10 0.000536\n", - " 1987-11-17 0.000714\n", - " 1988-02-12 0.000714\n", - " 1988-05-16 0.000714\n", - " ... ...\n", - " 2024-11-08 0.250000\n", - " 2025-02-10 0.250000\n", - " 2025-05-12 0.260000\n", - " 2025-08-11 0.260000\n", - " 2025-11-10 0.260000\n", - " \n", - " [89 rows x 1 columns]}" - ] - }, - "execution_count": 78, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "hist = get_div_histories([\"aapl\"])\n", - "hist" - ] - }, - { - "cell_type": "code", - "execution_count": 145, - "id": "627126c5", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[autoreload of trade.optionlib.assets.dividend failed: Traceback (most recent call last):\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 276, in check\n", - " superreload(m, reload, self.old_objects)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 500, in superreload\n", - " update_generic(old_obj, new_obj)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 397, in update_generic\n", - " update(a, b)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 365, in update_class\n", - " update_instances(old, new)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 323, in update_instances\n", - " object.__setattr__(ref, \"__class__\", new)\n", - "TypeError: __class__ assignment: 'ScheduleEntry' object layout differs from 'ScheduleEntry'\n", - "]\n" - ] - }, - { - "data": { - "text/plain": [ - "([], [], Timestamp('2026-11-11 00:00:00'))" - ] - }, - "execution_count": 145, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "_dual_project_dividends(\n", - " valuation_date=\"2026-11-11\",\n", - " end_date=\"2026-12-31\",\n", - " div_history=hist['aapl'],\n", - " inferred_growth_rate=0.10\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "813023d4", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[(datetime.date(2024, 2, 9), 0.24), (datetime.date(2024, 5, 10), 0.25), (datetime.date(2024, 8, 12), 0.25), (datetime.date(2024, 11, 8), 0.25), (datetime.date(2025, 2, 10), 0.25), (datetime.date(2025, 5, 12), 0.26), (datetime.date(2025, 8, 11), 0.26), (datetime.date(2025, 11, 10), 0.26), (datetime.date(2026, 2, 10), np.float64(0.26)), (datetime.date(2026, 5, 10), np.float64(0.26285714285714284)), (datetime.date(2026, 8, 10), np.float64(0.26574568288854006)), (datetime.date(2026, 11, 10), np.float64(0.26866596511808444))]\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[autoreload of trade.optionlib.assets.dividend failed: Traceback (most recent call last):\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 276, in check\n", - " superreload(m, reload, self.old_objects)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 500, in superreload\n", - " update_generic(old_obj, new_obj)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 397, in update_generic\n", - " update(a, b)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 365, in update_class\n", - " update_instances(old, new)\n", - " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 323, in update_instances\n", - " object.__setattr__(ref, \"__class__\", new)\n", - "TypeError: __class__ assignment: 'ScheduleEntry' object layout differs from 'ScheduleEntry'\n", - "]\n" - ] - }, - { - "data": { - "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "div_schedule = get_vectorized_dividend_scehdule(\n", - " tickers = [\"AAPL\"],\n", - " end_dates = [\"2026-12-31\"],\n", - " start_dates=[\"2024-01-01\"],\n", - " method=DiscreteDivGrowthModel.CONSTANT_AVG.value,\n", + "from trade.helpers.helper import compare_dates, get_missing_dates\n", + "from trade.helpers.Logging import setup_logger\n", + "from datetime import datetime\n", + "from pandas.tseries.offsets import BDay\n", + "from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE\n", + "from trade.helpers.decorators import cProfiler\n", + "from trade.helpers.helper import print_top_cprofile_stats\n", + "from trade.optionlib.assets.forward import (\n", + " vectorized_discrete_pv,\n", + " vectorized_forward_continuous,\n", + " vectorized_forward_discrete,\n", + " get_vectorized_continuous_dividends,\n", ")\n", "\n", - "div_schedule[0].schedule" - ] - }, - { - "cell_type": "code", - "execution_count": 208, - "id": "0524b12c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\u001b[0;31mSignature:\u001b[0m\n", - "\u001b[0mget_vectorized_dividend_rate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mtickers\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mList\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mspots\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mList\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfloat\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mvaluation_dates\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mList\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfloat\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Get the vectorized dividend rate for a list of tickers based on their historical dividend data.\n", - "\n", - "tickers: str or List[str] - Ticker symbols of the stocks.\n", - "spots: List[float] - Current spot prices for each ticker.\n", - "valuation_dates: List[datetime] - Dates for which to calculate the dividend rates.\n", - "\n", - "Returns a numpy array of dividend rates.\n", - "\u001b[0;31mFile:\u001b[0m ~/cloned_repos/QuantTools/trade/optionlib/assets/dividend.py\n", - "\u001b[0;31mType:\u001b[0m function" - ] - } - ], - "source": [ - "get_vectorized_dividend_rate(\n", - " t\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "cada1f96", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[]" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "frac = vector_convert_to_time_frac(schedules=div_schedule, valuation_dates=[\"2024-01-01\"], end_dates=[\"2026-12-31\"])\n", - "# pv = vectorized_discrete_pv(\n", - "# schedules=div_schedule,\n", - "# r = [0.05] * len(div_schedule),\n", - "# _valuation_dates=[\"2024-01-01\"],\n", - "# _end_dates=[\"2026-12-31\"]\n", + "from trade.optionlib.vol.implied_vol import (\n", + " vector_vol_estimation,\n", + " bsm_vol_est_brute_force,\n", + " estimate_crr_implied_volatility,\n", + " crr_binomial_pricing\n", + ")\n", + "\n", + "from trade.optionlib.pricing.binomial import vector_crr_binomial_pricing\n", + "\n", + "from trade.optionlib.utils.batch_operation import vector_batch_processor\n", + "from trade.assets.rates import get_risk_free_rate_helper, _fetch_rates\n", + "import time\n", + "# from dbase.DataAPI.ThetaData import (\n", + "# retrieve_bulk_eod,\n", + "# retrieve_eod_ohlc,\n", + "# retrieve_quote,\n", + "# list_contracts,\n", "# )\n", - "# pv\n", "\n", - "frac" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "2fe3aca7", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[(0.10677618069815195, 0.24),\n", - " (0.35592060232717315, 0.25),\n", - " (0.6132785763175906, 0.25),\n", - " (0.8542094455852156, 0.25),\n", - " (1.111567419575633, 0.25),\n", - " (1.3607118412046544, 0.26),\n", - " (1.6098562628336757, 0.26),\n", - " (1.8590006844626967, 0.26),\n", - " (2.11088295687885, np.float64(0.26)),\n", - " (2.3545516769336072, np.float64(0.26285714285714284)),\n", - " (2.6064339493497606, np.float64(0.26574568288854006)),\n", - " (2.858316221765914, np.float64(0.26866596511808444))]" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "frac[0].schedule" + "from dbase.DataAPI.ThetaData.v2 import (\n", + " retrieve_bulk_eod,\n", + " retrieve_eod_ohlc,\n", + " retrieve_quote,\n", + " list_contracts,\n", + " quote_to_eod_patch,\n", + " list_dates,\n", + ")\n", + "\n", + "from trade.optionlib.utils.format import (\n", + " assert_equal_length, \n", + " convert_to_array\n", + ")\n", + "\n", + "\n", + "from dbase.DataAPI.ThetaData.utils import _handle_opttick_param\n", + "from dbase.utils import default_timestamp\n", + "logger = setup_logger(__name__, stream_log_level=\"INFO\")\n", + "DM_GEN_PATH = Path(os.getenv(\"GEN_CACHE_PATH\")) / \"dm_gen_cache\"\n", + "TS = MarketTimeseries(_end=datetime.now())" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 43, "id": "87eb5648", "metadata": {}, "outputs": [], @@ -403,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "8f6e98be", "metadata": {}, "outputs": [], @@ -415,6 +141,8 @@ " RATES = \"rates\"\n", " DIVS = \"divs\"\n", " FWD = \"forward\"\n", + " OPTION_SPOT = \"option_spot\"\n", + " DATES = \"dates\"\n", "\n", " # Volatility\n", " IV = \"iv\"\n", @@ -428,6 +156,7 @@ " THETA = \"theta\"\n", " VOMMA = \"vomma\"\n", " VANNA = \"vanna\"\n", + " RHO = \"rho\"\n", "\n", "\n", "def _norm_str(x: str) -> str:\n", @@ -439,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 45, "id": "b3d6d001", "metadata": {}, "outputs": [ @@ -487,6 +216,7 @@ " series_id: SeriesId,\n", " **extra_parts: Any,\n", ") -> str:\n", + " \"\"\"Constructs deterministic cache key from symbol, interval, artifact type, series ID, and extra parts.\"\"\"\n", " \n", " if series_id in (SeriesId.AT_TIME, SeriesId.SNAPSHOT):\n", " assert 'time' in extra_parts, \"time must be provided for at_time or snapshot series_id\"\n", @@ -521,7 +251,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 46, "id": "9a81a0f1", "metadata": {}, "outputs": [ @@ -535,6 +265,7 @@ ], "source": [ "def _parse_cache_key(key: str) -> Dict[str, str]:\n", + " \"\"\"Parses a pipe-delimited cache key into a dictionary of key-value pairs.\"\"\"\n", " parts = key.split(\"|\")\n", " result = {}\n", " for part in parts:\n", @@ -555,7 +286,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 47, "id": "38f19bc8", "metadata": {}, "outputs": [ @@ -566,7 +297,7 @@ " PosixPath('/Users/chiemelienwanisobi/cloned_repos/QuantTools/.cache/dm_gen_cache'))" ] }, - "execution_count": 22, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -577,7 +308,7 @@ }, { "cell_type": "code", - "execution_count": 165, + "execution_count": 48, "id": "608cffd6", "metadata": {}, "outputs": [], @@ -585,7 +316,6 @@ "\n", "\n", "from abc import ABC\n", - "from dataclasses import dataclass\n", "from typing import Any, Callable, ClassVar, Dict, Optional, Type, TypeVar\n", "\n", "# Assumes you already have these (from your cache_key module)\n", @@ -594,6 +324,7 @@ "T = TypeVar(\"T\")\n", "\n", "\n", + "# REMEBER: Take out the commented out parts\n", "@dataclass(frozen=True, slots=True)\n", "class CacheSpec:\n", " \"\"\"\n", @@ -640,6 +371,7 @@ " _CACHE_NAME_REGISTRY: ClassVar[Dict[str, Type[\"BaseDataManager\"]]] = {}\n", "\n", " def __init_subclass__(cls, **kwargs: Any) -> None:\n", + " \"\"\"Enforces that all subclasses define CACHE_NAME and DEFAULT_SERIES_ID.\"\"\"\n", " super().__init_subclass__(**kwargs)\n", "\n", " # Skip enforcement for the abstract base itself\n", @@ -690,6 +422,9 @@ " expire_days=self.cache_spec.default_expire_days,\n", " clear_on_exit=self.cache_spec.clear_on_exit)\n", " self.enable_namespacing = enable_namespacing\n", + " out = self.cache.expire()\n", + " if out > 0:\n", + " logger.info(f\"{self.CACHE_NAME} has expired {out} entries\")\n", "\n", " # Key construction\n", " def make_key(\n", @@ -778,191 +513,488 @@ ] }, { - "cell_type": "markdown", - "id": "7cd6e535", + "cell_type": "code", + "execution_count": 49, + "id": "6bd11440", "metadata": {}, + "outputs": [], "source": [ - "### Dividends DataManager" + "DATE_HINT = Union[datetime, str]\n", + "\n", + "@dataclass(slots=True)\n", + "class DateRangePacket:\n", + " \"\"\"\n", + " Simple container for start/end date ranges with both datetime and string formats.\n", + " \"\"\"\n", + " start_date: DATE_HINT\n", + " end_date: DATE_HINT\n", + " start_str: Optional[str] = None\n", + " end_str: Optional[str] = None\n", + " maturity_date: DATE_HINT = None\n", + " maturity_str: Optional[str] = None\n", + "\n", + " def __post_init__(self):\n", + "\n", + " self.start_date = to_datetime(self.start_date)\n", + " self.end_date = to_datetime(self.end_date)\n", + " if self.maturity_date is not None:\n", + " self.maturity_date = to_datetime(self.maturity_date)\n", + "\n", + " self.start_str = self.start_str or self.start_date.strftime(\"%Y-%m-%d\")\n", + " self.end_str = self.end_str or self.end_date.strftime(\"%Y-%m-%d\")\n", + " if self.maturity_date is not None:\n", + " self.maturity_str = self.maturity_str or self.maturity_date.strftime(\"%Y-%m-%d\")\n", + " else:\n", + " self.maturity_str = None" ] }, { "cell_type": "code", - "execution_count": 166, - "id": "1b6bce12", + "execution_count": 50, + "id": "f7e0ab8b", "metadata": {}, "outputs": [], "source": [ - "\n", - "@dataclass\n", - "class DividendsConfig(metaclass=SingletonMetaClass):\n", - " default_lookback_years: int = DIVIDEND_LOOKBACK_YEARS\n", - " default_forecast_method: DiscreteDivGrowthModel = DiscreteDivGrowthModel.CONSTANT\n", - " dividend_type: DivType = DivType.DISCRETE\n", - " include_special_dividends: bool = False\n", - "\n", - " def assert_valid(self) -> None:\n", - " assert self.default_lookback_years > 0, \"Lookback years must be positive.\"\n", - " assert self.default_lookback_years <= 5, \"Lookback years seems too large. Max 5.\"\n", - " assert isinstance(self.default_forecast_method, DiscreteDivGrowthModel), \"Invalid forecast method. Expected DiscreteDivGrowthModel Enum.\"\n", - " assert isinstance(self.dividend_type, DivType), \"Invalid dividend type. Expected DivType Enum.\"\n", - " assert isinstance(self.include_special_dividends, bool), \"include_special_dividends must be a boolean.\"\n", + "from bisect import bisect_left, bisect_right\n", + "from datetime import date\n", + "from typing import List\n", "\n", "\n", - "\n", - "\n", - " def __post_init__(self) -> None:\n", - " self.assert_valid()\n", - "\n", - " def __setattr__(self, name, value):\n", - " super().__setattr__(name, value)\n", - " self.assert_valid()\n" + "def slice_schedule(full_schedule: List, val_date: date, mat_date: date) -> List:\n", + " \"\"\"\n", + " Return entries in full_schedule with entry.date in [val_date, mat_date].\n", + " Assumes full_schedule is sorted by entry.date ascending and each entry has .date (datetime.date).\n", + " \"\"\"\n", + " dates = [e.date for e in full_schedule] # small list; ok to rebuild (or precompute once)\n", + " i0 = bisect_left(dates, val_date)\n", + " i1 = bisect_right(dates, mat_date)\n", + " return full_schedule[i0:i1]\n" ] }, { "cell_type": "code", - "execution_count": 168, - "id": "12698670", + "execution_count": 51, + "id": "4ae2ccd6", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{(1, 2), (2, 3)}" - ] - }, - "execution_count": 168, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "set([(1,2), (2,3), (1,2)])" + "from trade.helpers.helper import ny_now\n", + "def _should_save_today(max_date: date) -> bool:\n", + " \"\"\"\n", + " Determines if data should be saved today based on the max_date and current time in New York.\n", + " \"\"\"\n", + " today = date.today()\n", + " current_hour = ny_now().hour\n", + " return max_date >= today and current_hour >= 16" ] }, { "cell_type": "code", - "execution_count": 169, - "id": "83047047", + "execution_count": 52, + "id": "1aba7774", "metadata": {}, "outputs": [], "source": [ - "## How dividends timeseries will work:\n", - "## If discrete:\n", - " ## All constant(+...) will cache up to < today\n", - " ## All None Constant will not cache\n", - "## If continuous:\n", - " ## Rely on MarktetTimeseries to provide continuous dividend yield history. It already caches.\n", - "\n", - "\n", - " " + "def is_available_on_date(date: date) -> bool:\n", + " \"\"\"\n", + " Returns True if the given date is a business day and not a US holiday, False otherwise.\n", + " \"\"\"\n", + " date = to_datetime(date).strftime(\"%Y-%m-%d\")\n", + " return is_busday(date) and not is_USholiday(date)" ] }, { "cell_type": "code", - "execution_count": 234, - "id": "4fee6e66", + "execution_count": 53, + "id": "e5c7ddb2", "metadata": {}, "outputs": [], "source": [ - "class DividendDataManager(BaseDataManager):\n", - " CACHE_NAME: ClassVar[str] = \"dividend_data_manager\"\n", - " DEFAULT_SERIES_ID: ClassVar[\"SeriesId\"] = SeriesId.HIST\n", - " CONFIG = DividendsConfig()\n", + "\n", + "def _data_structure_cache_it(self: BaseDataManager, \n", + " key: str, \n", + " value: Union[pd.Series, pd.DataFrame],\n", + " *, \n", + " expire: Optional[int] = None):\n", + " \"\"\"Merges and caches rate timeseries, excluding today's partial data.\"\"\"\n", + " value = value.copy()\n", + " if not isinstance(value, (pd.Series, pd.DataFrame)):\n", + " raise TypeError(f\"Expected pd.Series or pd.DataFrame for caching, got {type(value)}\")\n", " \n", - " def __init__(self, symbol: str, *, cache_spec: Optional[CacheSpec] = None, enable_namespacing: bool = False) -> None:\n", - " super().__init__(cache_spec=cache_spec, enable_namespacing=enable_namespacing)\n", - " self.symbol = symbol\n", + " if not isinstance(value.index, pd.DatetimeIndex):\n", + " raise TypeError(\"Expected DatetimeIndex for caching timeseries data.\")\n", + " \n", + " if not isinstance(self, BaseDataManager):\n", + " raise TypeError(f\"{self.__class__.__name__} must be a subclass of BaseDataManager.\")\n", + " \n", + " \n", + " ## Since it is a timeseries, we will append to existing if exists\n", + " existing = self.get(key, default=None)\n", + " if existing is not None:\n", + " # Merge existing and new values. We're expecting pd.Series\n", + " merged = pd.concat([existing, value])\n", + " value = merged[~merged.index.duplicated(keep=\"last\")]\n", "\n", - " def cache_it(self, key: str, value: Any, *, expire: Optional[int] = None, _type: str = \"discrete\") -> None:\n", + " if value.empty:\n", + " logger.info(f\"Not caching empty timeseries for key: {key}\")\n", + " return\n", "\n", - " \n", - " ## If discrete dividends, we first check if key exists\n", - " ## If it does, we add to it. Only values <= today.\n", - " ## If it does not, we create new entry\n", - " if _type == \"discrete\":\n", - " existing = self.get(key, default=None)\n", - " allowed = [entry for entry in value if entry.date <= datetime.now().date()]\n", - " if existing is not None:\n", - " # Merge existing and new values. We're expecting lists of ScheduleEntry\n", - " merged = existing + allowed\n", - " \n", - " ## Unique by date\n", - " uniques = list({entry.date: entry for entry in merged}.values())\n", - " self.set(key, uniques, expire=expire)\n", - " return\n", - " else:\n", - " self.set(key, allowed, expire=expire)\n", - " return\n", + " if not _should_save_today(max_date=value.index.max().date()):\n", + " logger.info(f\"Cutting off today's data for key: {key} to avoid saving partial day data.\")\n", + " value = value[value.index < pd.to_datetime(date.today())]\n", "\n", - " # For other types or if no existing, just setattr\n", - " self.set(key, value, expire=expire)\n", + " value.sort_index(inplace=True)\n", "\n", - " @overload\n", - " def get_data(self, \n", - " start_date: Union[datetime, str],\n", - " end_date: Union[datetime, str],\n", - " div_type: Optional[DivType] = None,\n", - " return_key: Literal[True] = True\n", - " ) -> tuple[pd.Series | List[ScheduleEntry], str]: ...\n", - "\n", - " @overload\n", - " def get_data(self, \n", - " start_date: Union[datetime, str],\n", - " end_date: Union[datetime, str],\n", - " div_type: Optional[DivType] = None,\n", - " return_key: Literal[False] = False\n", - " ) -> pd.Series | List[ScheduleEntry]: ...\n", - "\n", - " def get_data(self, \n", - " start_date: Union[datetime, str],\n", - " end_date: Union[datetime, str],\n", - " div_type: Optional[DivType] = None,\n", - " return_key: bool = False\n", - " ) -> pd.Series | List[ScheduleEntry] | tuple[pd.Series | List[ScheduleEntry], str]:\n", - " \"\"\"\n", - " Get dividend data for the symbol. Ensures internal caching logic is respected.\n", + " self.set(key, value, expire=expire)\n", "\n", - " Parameters\n", - " ----------\n", - " start_date : Union[datetime, str]\n", - " Start date for dividend data retrieval.\n", - " end_date : Union[datetime, str]\n", - " End date for dividend data retrieval.\n", - " Returns\n", - " -------\n", - " pd.Series | List[ScheduleEntry]\n", - " Dividend data in the appropriate format.\n", - " \"\"\"\n", - " div_type = DivType(div_type) if div_type is not None else self.CONFIG.dividend_type\n", - " if div_type == DivType.DISCRETE:\n", - " data, key = self.get_discrete_dividend_schedule(\n", - " ticker=self.symbol,\n", - " start_date=start_date.strftime(\"%Y-%m-%d\") if isinstance(start_date, datetime) else start_date,\n", - " end_date=end_date.strftime(\"%Y-%m-%d\") if isinstance(end_date, datetime) else end_date,\n", - " )\n", - " elif div_type == DivType.CONTINUOUS:\n", - " data = self.get_div_yield_history(self.symbol)\n", - " key = None \n", "\n", - " else:\n", - " raise ValueError(f\"Unsupported dividend type: {div_type}\")\n", + "def _simple_list_cache_it(\n", + " self: BaseDataManager,\n", + " key: str,\n", + " value: List[Any],\n", + " *,\n", + " expire: Optional[int] = None\n", + "):\n", + " \"\"\"Cache a list of simple values. Will append and keep unique. Also sort\"\"\"\n", + "\n", + " if not isinstance(value, list):\n", + " raise TypeError(f\"Expected list. Recieved {type(value)}\")\n", + " \n", + " existing: List = self.get(key, default = [])\n", + " existing.extend(value)\n", + " existing = sorted(list(set(existing)))\n", + " self.set(key, value, expire=expire)" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "c2018f93", + "metadata": {}, + "outputs": [], + "source": [ + "def _data_structure_sanitize(df: Union[pd.Series, pd.DataFrame],\n", + " start: Union[datetime, str],\n", + " end: Union[datetime, str],) -> Union[pd.Series, pd.DataFrame]:\n", + " \"\"\"Sanitizes the data structure by removing duplicates and sorting the index.\"\"\"\n", + " print(f\"Sanitizing data from {start} to {end}...\")\n", + " if not isinstance(df, (pd.Series, pd.DataFrame)):\n", + " raise TypeError(f\"Expected pd.Series or pd.DataFrame for sanitization, got {type(df)}\")\n", + " \n", + " # Ensure DatetimeIndex. If not, attempt conversion\n", + " if not isinstance(df.index, pd.DatetimeIndex):\n", + " try: \n", + " df.index = to_datetime(df.index, format=\"%Y-%m-%d\")\n", + " except Exception as e:\n", + " raise TypeError(\"Expected DatetimeIndex for sanitization of timeseries data.\") from e\n", " \n", - " if return_key:\n", - " return data, key\n", - " return data \n", + " \n", + " # Remove duplicates, keeping the last occurrence\n", + " df = df[~df.index.duplicated(keep=\"last\")]\n", + " \n", + " # Sort the index\n", + " df = df.sort_index()\n", + "\n", + " # if dataframe, lower case columns\n", + " if isinstance(df, pd.DataFrame):\n", + " df.columns = df.columns.str.lower()\n", + "\n", + " # Filter by start and end dates\n", + " df = df[(df.index >= pd.to_datetime(start)) & (df.index <= pd.to_datetime(end))]\n", + "\n", + " # Re-sort after filtering\n", + " df = df.sort_index()\n", + "\n", + " # Index name=datetime\n", + " df.index.name = \"datetime\"\n", + " \n", + " return df" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "9089c70a", + "metadata": {}, + "outputs": [], + "source": [ + "def _check_cache_for_timeseries_data_structure(\n", + " self: BaseDataManager,\n", + " key: str,\n", + " start_dt: DATE_HINT,\n", + " end_dt: DATE_HINT,\n", + ") -> Tuple[Optional[Union[pd.Series, pd.DataFrame]], bool, DATE_HINT, DATE_HINT]:\n", + " \"\"\"\n", + " Checks cache for existing timeseries data structure and identifies missing dates.\n", + "\n", + " Return args order:\n", + " - cached_data: The cached pd.Series or pd.DataFrame if fully present, else None\n", + " - is_partial: True if some dates are missing, False if fully present\n", + " - missing_start_date: The earliest missing date if partially present, else start_dt\n", + " - missing_end_date: The latest missing date if partially present, else end_dt\n", + " \"\"\"\n", + "\n", + " cached_data = self.get(key, default=None)\n", + " if not isinstance(self, BaseDataManager):\n", + " raise TypeError(f\"{self.__class__.__name__} must be a subclass of BaseDataManager.\")\n", + " \n", + " if not isinstance(cached_data, (pd.Series, pd.DataFrame, type(None))):\n", + " return None, False, start_dt, end_dt\n", + " \n", + " if cached_data is None:\n", + " return None, False, start_dt, end_dt\n", + " \n", + " missing = get_missing_dates(cached_data, _start=start_dt, _end=end_dt)\n", + " if not missing:\n", + " logger.info(f\"Cache hit for timeseries data structure key: {key}\")\n", + " cached_data = _data_structure_sanitize(\n", + " cached_data,\n", + " start=start_dt,\n", + " end=end_dt,\n", + " )\n", + " return cached_data, False, start_dt, end_dt\n", + " logger.info(\n", + " f\"Cache partially covers requested date range for timeseries data structure. \"\n", + " f\"Key: {key}. Fetching missing dates: {missing}\"\n", + " )\n", + " return cached_data, True, min(missing), max(missing)\n" + ] + }, + { + "cell_type": "markdown", + "id": "7cd6e535", + "metadata": {}, + "source": [ + "### Dividends DataManager" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "1b6bce12", + "metadata": {}, + "outputs": [], + "source": [ + "class OptionSpotEndpointSource(Enum):\n", + " \"\"\"\n", + " Thetadata creates a native EOD report every day by 6pm ET.\n", + " This enum allows choosing between using that EOD report or the intraday quote end point.\n", + " This is essential because during market hours, the EOD report is not yet available.\n", + " \"\"\"\n", + "\n", + " EOD = \"eod\"\n", + " QUOTE = \"quote\"\n", + "\n", + "class OptionPricingModel(Enum):\n", + " \"\"\"Enumeration of option pricing model.\"\"\"\n", + "\n", + " BSM = \"Black-Scholes\"\n", + " BINOMIAL = \"Binomial\"\n", + "\n", + "\n", + "class VolatilityModel(Enum):\n", + " \"\"\"Enumeration of volatility model.\"\"\"\n", + "\n", + " MARKET = \"market\"\n", + " MODEL_DYNAMIC = \"model_dynamic\"\n", + "\n", + "\n", + "@dataclass\n", + "class OptionDataConfig(metaclass=SingletonMetaClass):\n", + " \"\"\"Configuration for OptionDataManager.\"\"\"\n", + "\n", + " option_spot_endpoint_source: OptionSpotEndpointSource = OptionSpotEndpointSource.EOD\n", + " default_lookback_years: int = DIVIDEND_LOOKBACK_YEARS\n", + " default_forecast_method: DiscreteDivGrowthModel = DiscreteDivGrowthModel.CONSTANT\n", + " dividend_type: DivType = DivType.DISCRETE\n", + " include_special_dividends: bool = False\n", + " option_model: OptionPricingModel = OptionPricingModel.BSM\n", + " volatility_model: VolatilityModel = VolatilityModel.MARKET\n", + "\n", + " def assert_valid(self) -> None:\n", + " \"\"\"Validates all configuration values against business rules.\"\"\"\n", + " assert self.default_lookback_years > 0, \"Lookback years must be positive.\"\n", + " assert self.default_lookback_years <= 5, \"Lookback years seems too large. Max 5.\"\n", + " assert isinstance(\n", + " self.default_forecast_method, DiscreteDivGrowthModel\n", + " ), \"Invalid forecast method. Expected DiscreteDivGrowthModel Enum.\"\n", + " assert isinstance(self.dividend_type, DivType), \"Invalid dividend type. Expected DivType Enum.\"\n", + " assert isinstance(self.include_special_dividends, bool), \"include_special_dividends must be a boolean.\"\n", + " assert isinstance(self.option_spot_endpoint_source, OptionSpotEndpointSource), (\n", + " \"Invalid option_spot_endpoint_source. Expected OptionSpotEndpointSource Enum.\"\n", + " )\n", + " assert isinstance(self.option_model, OptionPricingModel), \"Invalid option_model. Expected OptionPricingModel Enum.\"\n", + " assert isinstance(self.volatility_model, VolatilityModel), \"Invalid volatility_model. Expected VolatilityModel Enum.\"\n", + "\n", + " def __post_init__(self) -> None:\n", + " \"\"\"Validates configuration after initialization.\"\"\"\n", + " self.assert_valid()\n", + "\n", + " def __setattr__(self, name, value):\n", + " \"\"\"Validates configuration after any attribute change.\"\"\"\n", + " super().__setattr__(name, value)\n", + " self.assert_valid()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "83047047", + "metadata": {}, + "outputs": [], + "source": [ + "## How dividends timeseries will work:\n", + "## If discrete:\n", + " ## All constant(+...) will cache up to < today\n", + " ## All None Constant will not cache\n", + "## If continuous:\n", + " ## Rely on MarktetTimeseries to provide continuous dividend yield history. It already caches.\n", + "\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "01570149", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class Result:\n", + " \"\"\"Base class for all data manager result containers.\"\"\"\n", + "\n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides additional fields for string representation. Override in subclasses.\"\"\"\n", + " return {}\n", + " \n", + " def __repr__(self) -> str:\n", + " \"\"\"Returns string representation with additional fields from subclass.\"\"\"\n", + " additional_fields = self._additional_repr_fields()\n", + " if additional_fields:\n", + " fields_str = \", \".join(f\"{k}={v!r}\" for k, v in additional_fields.items())\n", + " return f\"{self.__class__.__name__}({fields_str})\"\n", + " return f\"{self.__class__.__name__}()\"\n", + "\n", + "@dataclass\n", + "class DividendsResult(Result):\n", + " \"\"\"Contains dividend schedule or yield data for a date range.\"\"\"\n", + " daily_discrete_dividends: Optional[pd.Series] = None\n", + " daily_continuous_dividends: Optional[pd.Series] = None\n", + " dividend_type: Optional[DivType] = None\n", + " key: Optional[str] = None\n", + " undo_adjust: Optional[bool] = None\n", + "\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()\n", + " \n", + " def is_empty(self) -> bool:\n", + " \"\"\"Checks if dividend data is missing or empty.\"\"\"\n", + " if self.dividend_type == DivType.DISCRETE:\n", + " return self.daily_discrete_dividends is None or self.daily_discrete_dividends.empty\n", + " elif self.dividend_type == DivType.CONTINUOUS:\n", + " return self.daily_continuous_dividends is None or self.daily_continuous_dividends.empty\n", + " return True\n", + " \n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides dividend-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"dividend_type\": self.dividend_type,\n", + " \"key\": self.key,\n", + " \"is_empty\": self.is_empty(),\n", + " \"undo_adjust\": self.undo_adjust,\n", + " }\n" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "4fee6e66", + "metadata": {}, + "outputs": [], + "source": [ + "class DividendDataManager(BaseDataManager):\n", + " \"\"\"Manages dividend data retrieval, caching, and schedule construction for a specific symbol.\"\"\"\n", + " CACHE_NAME: ClassVar[str] = \"dividend_data_manager\"\n", + " DEFAULT_SERIES_ID: ClassVar[\"SeriesId\"] = SeriesId.HIST\n", + " CONFIG = OptionDataConfig()\n", + " INSTANCES = {}\n", + "\n", + " def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> \"DividendDataManager\":\n", + " \"\"\"Returns cached instance for symbol, creating new one if needed.\"\"\"\n", + " if symbol not in cls.INSTANCES:\n", + " TS.load_timeseries(symbol, start_date=OPTION_TIMESERIES_START_DATE, end_date=datetime.now())\n", + " instance = super(DividendDataManager, cls).__new__(cls)\n", + " cls.INSTANCES[symbol] = instance\n", + " return cls.INSTANCES[symbol]\n", + " \n", + " def __init__(self, symbol: str, *, cache_spec: Optional[CacheSpec] = None, enable_namespacing: bool = False) -> None:\n", + " \"\"\"Initializes manager for a symbol with cache and temp cache for short-lived data.\"\"\"\n", + "\n", + " if getattr(self, \"_initialized\", False):\n", + " return\n", + " self._initialized = True\n", + " super().__init__(cache_spec=cache_spec, enable_namespacing=enable_namespacing)\n", + " self.symbol = symbol\n", + " self.temp_cache: CustomCache = CustomCache(location=DM_GEN_PATH.as_posix(), \n", + " fname=\"dividend_temp_cache\", \n", + " expire_days=1, \n", + " clear_on_exit=True)\n", + "\n", + " ## General caching logic\n", + " def cache_it(self, \n", + " key: str, \n", + " value: Any, \n", + " *, \n", + " expire: Optional[int] = None, \n", + " _type: str = \"discrete\") -> None:\n", + " \"\"\"Caches dividend data with merge logic for discrete dividends (no future dates).\"\"\"\n", "\n", " \n", + " ## If discrete dividends, we first check if key exists\n", + " ## If it does, we add to it. Only values <= today.\n", + " ## If it does not, we create new entry\n", + " if _type == \"discrete\":\n", + " existing = self.get(key, default=None)\n", + " today = datetime.today().date()\n", + " allowed = [e for e in value if e.date <= today]\n", + "\n", + " if existing is not None:\n", + " # Merge existing and new values. We're expecting lists of ScheduleEntry\n", + " merged = existing + allowed\n", + " \n", + " ## Unique by date\n", + " merged = {entry.date: entry for entry in merged}\n", + " uniques = sorted(merged.values(), key=lambda e: e.date)\n", + " self.set(key, uniques, expire=expire)\n", + " return\n", + " else:\n", + " self.set(key, allowed, expire=expire)\n", + " return\n", + "\n", + " # For other types or if no existing, just setattr\n", + " self.set(key, value, expire=expire)\n", + "\n", + " ## Dividend yield history retrieval for continuous dividends. Already cached in MarketTimeseries.\n", + " def get_div_yield_history(self, symbol: str, skip_preload_check: bool = False) -> pd.Series:\n", + " \"\"\"Retrieves continuous dividend yield history from MarketTimeseries.\"\"\"\n", + " div_history = TS.get_timeseries(symbol, skip_preload_check=skip_preload_check)\n", + " return div_history.dividend_yield\n", "\n", + " ## Discrete dividend schedule retrieval with caching.\n", " def get_discrete_dividend_schedule(\n", " self,\n", " *,\n", - " ticker: str,\n", - " end_date: str,\n", - " start_date: str,\n", - " valuation_date: Optional[str] = None,\n", - " ):\n", - "\n", - "\n", + " end_date: Union[str, datetime, pd.Timestamp],\n", + " start_date: Union[str, datetime, pd.Timestamp],\n", + " valuation_date: Optional[Union[str, datetime, pd.Timestamp]] = None,\n", + " ) -> Tuple[List[ScheduleEntry], str]:\n", + " \"\"\"Returns discrete dividend schedule between dates with partial cache support.\"\"\"\n", + " \n", + " start_str = datetime.strftime(start_date, \"%Y-%m-%d\") if isinstance(start_date, (datetime, pd.Timestamp)) else start_date\n", + " end_str = datetime.strftime(end_date, \"%Y-%m-%d\") if isinstance(end_date, (datetime, pd.Timestamp)) else end_date\n", + " ticker = self.symbol\n", " method = self.CONFIG.default_forecast_method.value\n", " lookback_years = self.CONFIG.default_lookback_years\n", " key = self.make_key(\n", @@ -973,24 +1005,29 @@ " lookback_years=lookback_years,\n", " current_state=\"schedule\",\n", " interval=Interval.NA,\n", + " vendor=\"yfinance\"\n", " )\n", "\n", " available_schedule = self.get(key, default=None)\n", - " if available_schedule is not None:\n", - " \n", + " if available_schedule:\n", + " logger.info(f\"Cache hit for key: {key}\")\n", " ## If max date in available schedule >= end_date, we can use cache\n", " max_cached_date = max(entry.date for entry in available_schedule)\n", " min_cached_date = min(entry.date for entry in available_schedule)\n", - " fully_covered = (min_cached_date <= datetime.strptime(start_date, \"%Y-%m-%d\").date()) and (max_cached_date >= datetime.strptime(end_date, \"%Y-%m-%d\").date())\n", + " fully_covered = (min_cached_date <= datetime.strptime(start_str, \"%Y-%m-%d\").date()) and (\n", + " max_cached_date >= datetime.strptime(end_str, \"%Y-%m-%d\").date()\n", + " )\n", " if fully_covered:\n", - " print(\"Cache fully covers requested date range.\")\n", - " \n", + " logger.info(f\"Cache fully covers requested date range. Key: {key}\")\n", + "\n", " ## Filter to requested date range\n", - " filtered_schedule = [entry for entry in available_schedule if start_date <= entry.date.strftime(\"%Y-%m-%d\") <= end_date]\n", + " start_dt = datetime.strptime(start_date, \"%Y-%m-%d\").date()\n", + " end_dt = datetime.strptime(end_date, \"%Y-%m-%d\").date()\n", + " filtered_schedule = [e for e in available_schedule if start_dt <= e.date <= end_dt]\n", " return filtered_schedule, key\n", " else:\n", - " print(\"Cache partially covers requested date range. Fetching missing data.\")\n", - " \n", + " logger.info(f\"Cache partially covers requested date range. Key: {key}. Fetching missing data.\")\n", + "\n", " schedule = get_vectorized_dividend_scehdule(\n", " tickers=[ticker],\n", " end_dates=[end_date],\n", @@ -1001,12 +1038,201 @@ " )\n", " raw_schedule = schedule[0].schedule\n", " self.cache_it(key, raw_schedule, _type=\"discrete\")\n", - " \n", + "\n", " return raw_schedule, key\n", - " \n", - " def get_div_yield_history(self, symbol: str) -> pd.Series:\n", - " div_history = TS.get_timeseries(symbol)\n", - " return div_history.dividend_yield\n", + "\n", + " ## Switcher to choose between constructing all the way or using cached pieces\n", + " def _get_discrete_schedule_timeseries(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " maturity_date: Union[datetime, str],\n", + " div_type: Optional[DivType] = None,\n", + " undo_adjust: bool = True,\n", + " ) -> Tuple[pd.Series, str]:\n", + " \"\"\"Builds daily dividend schedule series with partial cache merging and split adjustment.\"\"\"\n", + " logger.info(f\"Fetching discrete dividend schedule timeseries for {self.symbol} from {start_date} to {end_date} with maturity {maturity_date}\")\n", + " div_type = DivType(div_type) if div_type is not None else self.CONFIG.dividend_type\n", + " is_partial = False\n", + " start_dt = pd.to_datetime(start_date).date()\n", + " end_dt = pd.to_datetime(end_date).date()\n", + " mat_dt = pd.to_datetime(maturity_date).date()\n", + " start_str = datetime.strftime(start_dt, \"%Y-%m-%d\")\n", + " end_str = datetime.strftime(end_dt, \"%Y-%m-%d\")\n", + " mat_str = datetime.strftime(mat_dt, \"%Y-%m-%d\")\n", + " \n", + " if mat_dt < start_dt:\n", + " print(f\"Maturity date {mat_dt} is before start date {start_dt}\")\n", + " raise ValueError(\"maturity_date must be >= start_date\")\n", + "\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " artifact_type=ArtifactType.DIVS,\n", + " series_id=SeriesId.HIST,\n", + " method=self.CONFIG.default_forecast_method.value,\n", + " lookback_years=self.CONFIG.default_lookback_years,\n", + " current_state=\"schedule_timeseries\",\n", + " interval=Interval.EOD,\n", + " undo_adjust=undo_adjust,\n", + " maturity=mat_str,\n", + " )\n", + "\n", + " cached_series = self.get(key, default=None)\n", + " if cached_series is not None:\n", + " logger.info(f\"Cache hit for discrete schedule timeseries key: {key}\")\n", + " missing_dates = get_missing_dates(\n", + " cached_series,\n", + " start_str,\n", + " end_str\n", + " )\n", + " if not missing_dates:\n", + " logger.info(f\"Cache fully covers requested date range for timeseries. Key: {key}\")\n", + " cached_series = cached_series[\n", + " (cached_series.index >= pd.to_datetime(start_date)) \n", + " & (cached_series.index <= pd.to_datetime(end_date))\n", + " ]\n", + " return cached_series, key\n", + " else:\n", + " logger.info(f\"Cache partially covers requested date range for timeseries. Key: {key}. Fetching missing dates: {missing_dates}\")\n", + " start_str, end_str = min(missing_dates), max(missing_dates)\n", + " is_partial = True\n", + " else:\n", + " logger.info(f\"No cache found for discrete schedule timeseries key: {key}. Building from scratch.\")\n", + " \n", + " # Build from scratch for missing dates\n", + " # Fetch ONCE: all events from start_date to maturity_date\n", + " full_schedule, _ = self.get_discrete_dividend_schedule(\n", + " start_date=start_str,\n", + " end_date=mat_str,\n", + " valuation_date=start_str,\n", + " )\n", + "\n", + " # Build daily schedules efficiently using a moving pointer\n", + " series = {}\n", + " date_range = pd.date_range(start=start_dt, end=end_dt, freq=\"B\").strftime(\"%Y-%m-%d\")\n", + " for d in date_range:\n", + " if d in HOLIDAY_SET:\n", + " # Skip holidays\n", + " continue\n", + " d_date = datetime.strptime(d, \"%Y-%m-%d\").date()\n", + "\n", + " ## Simple filter approach\n", + " series[d_date] = Schedule(slice_schedule(full_schedule, d_date, mat_dt))\n", + " data = pd.Series(series, name=\"dividend_schedule\")\n", + " \n", + " # Back-adjust to represent cashflows as of valuation date. Ie undoing splits\n", + " if undo_adjust:\n", + " data = data.to_frame()\n", + " split_factors = TS._split_factor[self.symbol].copy()\n", + " data[\"split_factor\"] = split_factors\n", + " data[\"dividend_schedule\"] = data[\"dividend_schedule\"] * data[\"split_factor\"]\n", + " data = data[\"dividend_schedule\"]\n", + " \n", + " # Cache the constructed timeseries\n", + " if is_partial:\n", + " # Merge with existing cached series\n", + " merged = pd.concat([cached_series, data])\n", + " data = merged[~merged.index.duplicated(keep='last')]\n", + " \n", + " data = _data_structure_sanitize(data, start_date, end_date)\n", + " \n", + " self.set(key, data, expire=86400/2) # 12 hours expiry for timeseries cache\n", + " return data, key\n", + "\n", + " def get_schedule_timeseries(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " maturity_date: Union[datetime, str],\n", + " div_type: Optional[DivType] = None,\n", + " undo_adjust: bool = True,\n", + " ) -> DividendsResult:\n", + " \"\"\"\n", + " Returns a DAILY series (indexed by date) where each value is the dividend schedule\n", + " from that valuation date up to maturity_date.\n", + "\n", + " - start_date/end_date define the valuation date range\n", + " - maturity_date is the fixed horizon (e.g., option expiry)\n", + " \"\"\"\n", + "\n", + " div_type = DivType(div_type) if div_type is not None else self.CONFIG.dividend_type\n", + " result = DividendsResult()\n", + " result.dividend_type = div_type\n", + " result.undo_adjust = undo_adjust\n", + "\n", + " if div_type == DivType.DISCRETE:\n", + " data, key = self._get_discrete_schedule_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " maturity_date=maturity_date,\n", + " div_type=div_type,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " data.index = pd.to_datetime(data.index)\n", + " data.index.name = \"datetime\"\n", + " data = data[\n", + " (data.index >= pd.to_datetime(start_date)) \n", + " & (data.index <= pd.to_datetime(end_date))]\n", + " data = data.sort_index()\n", + " data = data.drop_duplicates()\n", + " result.daily_discrete_dividends = data\n", + " result.key = key\n", + "\n", + " elif div_type == DivType.CONTINUOUS:\n", + " start_str = pd.to_datetime(start_date).strftime(\"%Y-%m-%d\") if isinstance(start_date, datetime) else start_date\n", + " end_str = pd.to_datetime(end_date).strftime(\"%Y-%m-%d\") if isinstance(end_date, datetime) else end_date\n", + " yield_history = self.get_div_yield_history(self.symbol, skip_preload_check=True)\n", + " filtered = yield_history[(yield_history.index >= start_str) & (yield_history.index <= end_str)]\n", + " result.daily_continuous_dividends = filtered\n", + " result.key = None\n", + " return result\n", + " \n", + " ## RT Enabled\n", + " def get_schedule(\n", + " self,\n", + " valuation_date: Union[datetime, str],\n", + " maturity_date: Union[datetime, str],\n", + " div_type: Optional[DivType] = None,\n", + " undo_adjust: bool = True,\n", + " ) -> DividendsResult:\n", + " \"\"\"Returns dividend schedule for a single valuation date to maturity.\"\"\"\n", + " \n", + " \n", + " div_type = DivType(div_type) if div_type is not None else self.CONFIG.dividend_type\n", + "\n", + " val_str = valuation_date.strftime(\"%Y-%m-%d\") if isinstance(valuation_date, datetime) else valuation_date\n", + " mat_str = maturity_date.strftime(\"%Y-%m-%d\") if isinstance(maturity_date, datetime) else maturity_date\n", + "\n", + " if div_type == DivType.DISCRETE:\n", + " data, key = self.get_discrete_dividend_schedule(\n", + " start_date=val_str,\n", + " end_date=mat_str,\n", + " valuation_date=val_str, # optional, but consistent\n", + " )\n", + " if undo_adjust:\n", + " split_factor = TS._split_factor[self.symbol].loc[pd.to_datetime(val_str)]\n", + " else:\n", + " split_factor = 1.0\n", + " data = Schedule(schedule=[entry * split_factor for entry in data])\n", + " data = pd.Series({val_str: data})\n", + " elif div_type == DivType.CONTINUOUS:\n", + " data = self.get_div_yield_history(self.symbol)\n", + " data = data[(data.index >= pd.to_datetime(valuation_date)) & (data.index <= pd.to_datetime(maturity_date))]\n", + " key = None\n", + " else:\n", + " raise ValueError(f\"Unsupported dividend type: {div_type}\")\n", + "\n", + " result = DividendsResult()\n", + " \n", + " if div_type == DivType.DISCRETE:\n", + " result.daily_discrete_dividends = data\n", + " else:\n", + " result.daily_continuous_dividends = data\n", + " result.key = key\n", + " result.undo_adjust = undo_adjust\n", + " result.dividend_type = div_type\n", + " \n", + " return result\n", "\n", " def offload(self, *args: Any, **kwargs: Any) -> None:\n", " \"\"\"\n", @@ -1014,89 +1240,2385 @@ " \"\"\"\n", " print(f\"No offload logic implemented for {self.CACHE_NAME}\")\n", "\n", - "test = DividendDataManager(symbol=\"AAPL\")\n" + "\n", + "\n", + "\n" ] }, { "cell_type": "code", - "execution_count": 241, - "id": "1eef60d2", + "execution_count": 60, + "id": "85c8b42c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Cache fully covers requested date range.\n" + "2026-01-18 19:43:34 [test] trade.__init__ INFO: Signal function for `_on_exit` added to signal number 15.\n", + "2026-01-18 19:43:34 [test] trade.__init__ INFO: Signal function for `_on_exit` added to signal number 2.\n", + "2026-01-18 19:43:34 [test] trade.__init__ INFO: Exit handler `_on_exit` registered for normal program exit.\n", + "2026-01-18 19:43:34 [test] __main__ INFO: Cache hit for key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-01-18 19:43:34 [test] __main__ INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-01-18 19:43:34 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker AAPL\n", + "2026-01-18 19:43:35 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 17, for original valuation: 9. Size from historical divs: 16\n", + "2026-01-18 19:43:35 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 1\n", + "2026-01-18 19:43:35 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.26]\n", + "2026-01-18 19:43:35 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.22, 0.23, 0.23, 0.23, 0.23, 0.24, 0.24, 0.24, 0.24, 0.25, 0.25, 0.25, 0.25, 0.26, 0.26, 0.26, 0.26]\n", + "2026-01-18 19:43:35 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2022, 2, 4), datetime.date(2022, 5, 6), datetime.date(2022, 8, 5), datetime.date(2022, 11, 4), datetime.date(2023, 2, 10), datetime.date(2023, 5, 12), datetime.date(2023, 8, 11), datetime.date(2023, 11, 10), datetime.date(2024, 2, 9), datetime.date(2024, 5, 10), datetime.date(2024, 8, 12), datetime.date(2024, 11, 8), datetime.date(2025, 2, 10), datetime.date(2025, 5, 12), datetime.date(2025, 8, 11), datetime.date(2025, 11, 10), datetime.date(2026, 2, 10)]\n" ] }, { "data": { "text/plain": [ - "[,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ,\n", - " ]" + "([,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ],\n", + " 'symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE')" ] }, - "execution_count": 241, + "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "test.CONFIG.dividend_type = DivType.DISCRETE\n", - "test.get_data(\n", - " start_date=\"2012-10-08\",\n", - " end_date=\"2025-10-31\",\n", + "testdiv = DividendDataManager(symbol=\"AAPL\")\n", + "testdiv.get_discrete_dividend_schedule(\n", + " start_date=\"2024-01-01\",\n", + " end_date=\"2025-12-31\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "1eef60d2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:35 [test] __main__ INFO: Fetching discrete dividend schedule timeseries for AAPL from 2025-01-01 to 2026-01-14 with maturity 2026-10-29\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:35 [test] __main__ INFO: Cache hit for discrete schedule timeseries key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-10-29|method:CONSTANT|undo_adjust:1\n", + "2026-01-18 19:43:35 [test] __main__ INFO: Cache fully covers requested date range for timeseries. Key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-10-29|method:CONSTANT|undo_adjust:1\n", + "Discrete Dividends Schedule Timeseries:\n", + "datetime\n", + "2025-01-02 ((2025-02-10, 0.25), (2025-05-12, 0.26), (2025...\n", + "2025-01-03 ((2025-02-10, 0.25), (2025-05-12, 0.26), (2025...\n", + "2025-01-06 ((2025-02-10, 0.25), (2025-05-12, 0.26), (2025...\n", + "2025-01-07 ((2025-02-10, 0.25), (2025-05-12, 0.26), (2025...\n", + "2025-01-08 ((2025-02-10, 0.25), (2025-05-12, 0.26), (2025...\n", + " ... \n", + "2026-01-08 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-09 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-12 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-13 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-14 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "Name: dividend_schedule, Length: 259, dtype: object\n" + ] + } + ], + "source": [ + "testdiv.CONFIG.dividend_type = DivType.DISCRETE\n", + "d = testdiv.get_schedule_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-14\",\n", + " maturity_date=\"2026-10-29\",\n", + " undo_adjust=True,\n", + ")\n", + "\n", + "# d2 = testdiv.get_schedule(\n", + "# valuation_date=\"2026-01-15\",\n", + "# maturity_date=\"2026-05-31\",\n", + "# undo_adjust=False,\n", + "# )\n", + "print(\"Discrete Dividends Schedule Timeseries:\")\n", + "print(d.daily_discrete_dividends)\n", + "# print(\"Discrete Dividends Schedule at specific valuation date:\")\n", + "# print(d2.daily_discrete_dividends)" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "547d12bb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['AAPL', 'NVDA', 'TSLA', 'COST', 'AMZN', 'META', 'AMD', 'SBUX', 'NFLX', 'BA']" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TS._spot.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "36691b9b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "DividendsResult(dividend_type=, key='symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-10-29|method:CONSTANT|undo_adjust:1', is_empty=False, undo_adjust=True)" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "d" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "8137ae34", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2022-10-10 0.001665\n", + "2022-10-11 0.001683\n", + "2022-10-12 0.001690\n", + "2022-10-13 0.001635\n", + "2022-10-14 0.001690\n", + " ... \n", + "2025-10-27 0.000968\n", + "2025-10-28 0.000967\n", + "2025-10-29 0.000965\n", + "2025-10-30 0.000959\n", + "2025-10-31 0.000963\n", + "Length: 800, dtype: float64" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "testdiv.CONFIG.dividend_type = DivType.CONTINUOUS\n", + "d = testdiv.get_schedule_timeseries(\n", + " start_date=\"2022-10-08\",\n", + " end_date=\"2025-10-31\",\n", + " maturity_date=\"2025-10-31\",\n", + ")\n", + "d.daily_continuous_dividends" + ] + }, + { + "cell_type": "markdown", + "id": "0a8b4078", + "metadata": {}, + "source": [ + "## Rates Data" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "06b02699", + "metadata": {}, + "outputs": [], + "source": [ + "import yfinance as yf\n", + "def deannualize(annual_rate, periods=365):\n", + " \"\"\"Converts annual rate to per-period rate.\"\"\"\n", + " return (1 + annual_rate) ** (1 / periods) - 1\n", + "\n", + "@dataclass\n", + "class RatesResult(Result):\n", + " \"\"\"Contains risk-free rate data for a date range.\"\"\"\n", + " daily_risk_free_rates: Optional[pd.Series] = None\n", + " \n", + " def is_empty(self) -> bool:\n", + " \"\"\"Checks if rate data is missing or empty.\"\"\"\n", + " return self.daily_risk_free_rates is None or self.daily_risk_free_rates.empty\n", + " \n", + " def _additional_repr_fields(self):\n", + " \"\"\"Provides rate-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"is_empty\": self.is_empty(),\n", + " }\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()\n", + " \n", + "\n", + "class RatesDataManager(BaseDataManager):\n", + " \"\"\"Singleton manager for risk-free rate data from treasury bills (^IRX).\"\"\"\n", + " CACHE_NAME: ClassVar[str] = \"rates_data_manager\"\n", + " DEFAULT_SERIES_ID: ClassVar[\"SeriesId\"] = SeriesId.HIST\n", + " INSTANCE = None\n", + " DEFAULT_YFINANCE_TICKER = \"^IRX\" # 13 WEEK TREASURY BILL\n", + " CONFIG: OptionDataConfig = OptionDataConfig()\n", + " \n", + " def __new__(\n", + " cls,\n", + " *,\n", + " cache_spec: Optional[CacheSpec] = None,\n", + " enable_namespacing: bool = False,\n", + " ) -> \"RatesDataManager\":\n", + " \"\"\"Ensures only one instance exists (singleton pattern).\"\"\"\n", + " \n", + " if cls.INSTANCE is not None:\n", + " return cls.INSTANCE\n", + " instance = super(RatesDataManager, cls).__new__(cls)\n", + " cls.INSTANCE = instance\n", + " return instance\n", + " \n", + " def __init__(self, *, cache_spec: Optional[CacheSpec] = None, enable_namespacing: bool = False) -> None:\n", + " \"\"\"Initializes singleton instance once, skipping subsequent calls.\"\"\"\n", + " if getattr(self, \"_init_called\", False):\n", + " return\n", + " self._init_called = True\n", + " super().__init__(cache_spec=cache_spec, enable_namespacing=enable_namespacing)\n", + "\n", + " def get_rate(\n", + " self,\n", + " date: Union[datetime, str],\n", + " interval: Interval = Interval.EOD,\n", + " str_interval: Optional[str] = None,\n", + " ) -> RatesResult:\n", + " \"\"\"Returns risk-free rate for a single date.\"\"\"\n", + " \n", + " if not is_available_on_date(to_datetime(date).date()):\n", + " logger.warning(f\"Requested date {date} is not a business day or is a US holiday. Returning empty RatesResult.\")\n", + " return RatesResult(daily_risk_free_rates=pd.Series(dtype=float))\n", + " date_str = pd.to_datetime(date).strftime(\"%Y-%m-%d\") if isinstance(date, datetime) else date\n", + " \n", + " rates_data = self.get_risk_free_rate_timeseries(\n", + " start_date=date_str,\n", + " end_date=date_str,\n", + " interval=interval,\n", + " str_interval=str_interval,\n", + " )\n", + " rate = rates_data.daily_risk_free_rates\n", + " if rate is not None and not rate.empty:\n", + " rate = rate.iloc[0:1]\n", + " \n", + "\n", + " return RatesResult(daily_risk_free_rates=rate)\n", + "\n", + " def get_risk_free_rate_timeseries(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " interval: Interval = Interval.EOD,\n", + " str_interval: Optional[str] = None,\n", + " ) -> RatesResult:\n", + " \"\"\"Returns risk-free rate timeseries with partial cache support.\"\"\"\n", + " \n", + " start_str = pd.to_datetime(start_date).strftime(\"%Y-%m-%d\") if isinstance(start_date, datetime) else start_date\n", + " end_str = pd.to_datetime(end_date).strftime(\"%Y-%m-%d\") if isinstance(end_date, datetime) else end_date\n", + " \n", + " ## Make cache key\n", + " key = self.make_key(\n", + " symbol=self.DEFAULT_YFINANCE_TICKER,\n", + " artifact_type=ArtifactType.RATES,\n", + " series_id=SeriesId.HIST,\n", + " interval=interval,\n", + " )\n", + " \n", + " ## Determine yfinance interval\n", + " if not str_interval:\n", + " fn_interval = \"1d\" if interval == Interval.EOD else \"30m\"\n", + " else:\n", + " fn_interval = str_interval\n", + " \n", + " ## Check cache\n", + " series = self.get(key, default=None)\n", + "\n", + " ## Check if cache covers requested date range\n", + " if series is not None:\n", + " logger.info(f\"Cache hit for risk-free rate timeseries key: {key}\")\n", + " missing = get_missing_dates(\n", + " series,\n", + " pd.to_datetime(start_date).strftime(\"%Y-%m-%d\"),\n", + " pd.to_datetime(end_date).strftime(\"%Y-%m-%d\"),\n", + " )\n", + "\n", + "\n", + " ## If no missing dates, return cached series\n", + " if not missing:\n", + " logger.info(f\"Cache fully covers requested date range for risk-free rate timeseries. Key: {key}\")\n", + " series = _data_structure_sanitize(\n", + " series,\n", + " start=start_str,\n", + " end=end_str,\n", + " )\n", + " return RatesResult(daily_risk_free_rates=series)\n", + " else:\n", + " ## Fetch missing dates\n", + " start_date = min(missing)\n", + " end_date = max(missing)\n", + " logger.info(f\"Cache partially covers requested date range for risk-free rate timeseries. Key: {key}. Fetching missing dates: {missing}\")\n", + " else:\n", + " logger.info(f\"No cache found for risk-free rate timeseries key: {key}. Fetching from source.\")\n", + "\n", + "\n", + " # Fetch rates data\n", + " rates_data = self._query_yfinance(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " interval=fn_interval,\n", + " )[\"annualized\"]\n", + "\n", + " if series is not None:\n", + " # Merge with existing cached series\n", + " merged = pd.concat([series, rates_data])\n", + " rates_data = merged[~merged.index.duplicated(keep='last')]\n", + " \n", + " ## Cache the updated series\n", + " self.cache_it(key, rates_data)\n", + " \n", + " ## Sanitize before returning\n", + " rates_data = _data_structure_sanitize(\n", + " rates_data,\n", + " start=start_str, # Ensure only requested range\n", + " end=end_str,\n", + " )\n", + "\n", + " return RatesResult(rates_data)\n", + "\n", + " def cache_it(self, key, value, *, expire=None):\n", + " \"\"\"Merges and caches rate timeseries, excluding today's partial data.\"\"\"\n", + " ## Since it is a timeseries, we will append to existing if exists\n", + " _data_structure_cache_it(self, key, value, expire=expire)\n", + "\n", + " \n", + " def _query_yfinance(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " interval: str,\n", + " ) -> pd.DataFrame:\n", + " \"\"\"Fetches ^IRX treasury bill rates from yfinance and formats output.\"\"\"\n", + "\n", + " ## Date buffer to ensure we get all data\n", + " start_date = to_datetime(start_date) - pd.Timedelta(days=5)\n", + " end_date = to_datetime(end_date) + pd.Timedelta(days=5)\n", + " \n", + " data_min = yf.download(\n", + " \"^IRX\",\n", + " start=start_date,\n", + " end=end_date,\n", + " interval=interval,\n", + " progress=False,\n", + " multi_level_index=False,\n", + " )\n", + "\n", + " data_min.columns = data_min.columns.str.lower()\n", + " data_min[\"daily\"] = data_min[\"close\"].apply(deannualize)\n", + " data_min[\"annualized\"] = data_min[\"close\"] / 100\n", + " data_min[\"name\"] = \"^IRX\"\n", + " data_min[\"description\"] = \"13 WEEK TREASURY BILL\"\n", + " data_min.index.name = \"Datetime\"\n", + " data_min = data_min[[\"name\", \"description\", \"daily\", \"annualized\"]]\n", + " data_min = data_min[(data_min.index >= pd.to_datetime(start_date)) & (data_min.index <= pd.to_datetime(end_date))]\n", + " return data_min\n" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "7e8192d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:36 [test] __main__ INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "2026-01-18 19:43:36 [test] __main__ INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "Sanitizing data from 2026-01-09 to 2026-01-12...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-09 0.03513\n", + "2026-01-12 0.03533\n", + "Name: annualized, dtype: float64" + ] + }, + "execution_count": 66, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rt_manager = RatesDataManager()\n", + "\n", + "rates_result = rt_manager.get_risk_free_rate_timeseries(\n", + " start_date=\"2026-01-09\",\n", + " end_date=\"2026-01-12\",\n", + ")\n", + "rates_result.daily_risk_free_rates" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "03d1e179", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:36 [test] __main__ INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "2026-01-18 19:43:36 [test] __main__ INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "Sanitizing data from 2026-01-13 to 2026-01-13...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-13 0.0356\n", + "Name: annualized, dtype: float64" + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rt_manager.get_rate(\n", + " date=pd.Timestamp(\"2026-01-13\"),\n", + ").daily_risk_free_rates" + ] + }, + { + "cell_type": "markdown", + "id": "ebe3aaea", + "metadata": {}, + "source": [ + "## Forward Price (Mostly for black scholes)" + ] + }, + { + "cell_type": "markdown", + "id": "33758a10", + "metadata": {}, + "source": [ + "### Forward DataManager" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "d8646955", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class ForwardResult(Result):\n", + " \"\"\"Contains forward price data (discrete or continuous dividend model).\"\"\"\n", + " daily_discrete_forward: Optional[pd.Series] = None\n", + " daily_continuous_forward: Optional[pd.Series] = None\n", + " dividend_type: Optional[DivType] = None\n", + " key: Optional[str] = None\n", + " dividend_result: Optional[DividendsResult] = None\n", + " \n", + " def is_empty(self) -> bool:\n", + " \"\"\"Checks if forward price data is missing or empty.\"\"\"\n", + " if self.dividend_type == DivType.DISCRETE:\n", + " return self.daily_discrete_forward is None or self.daily_discrete_forward.empty\n", + " elif self.dividend_type == DivType.CONTINUOUS:\n", + " return self.daily_continuous_forward is None or self.daily_continuous_forward.empty\n", + " return True\n", + " \n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides forward-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"dividend_type\": self.dividend_type,\n", + " \"key\": self.key,\n", + " \"is_empty\": self.is_empty(),\n", + " }\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "1a1f0069", + "metadata": {}, + "outputs": [], + "source": [ + "class ForwardDataManager(BaseDataManager):\n", + " \"\"\"Manages forward price computation and caching for a specific symbol using spot, rates, and dividends.\"\"\"\n", + " CACHE_NAME: ClassVar[str] = \"forward_data_manager\"\n", + " DEFAULT_SERIES_ID: ClassVar[\"SeriesId\"] = SeriesId.HIST\n", + " INSTANCES = {}\n", + " # CONFIG = ForwardsConfig()\n", + "\n", + " def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> \"ForwardDataManager\":\n", + " \"\"\"Returns cached instance for symbol, creating new one if needed.\"\"\"\n", + " if symbol not in cls.INSTANCES:\n", + " TS.load_timeseries(symbol, start_date=OPTION_TIMESERIES_START_DATE, end_date=datetime.now())\n", + " instance = super(ForwardDataManager, cls).__new__(cls)\n", + " cls.INSTANCES[symbol] = instance\n", + " return cls.INSTANCES[symbol]\n", + "\n", + " def __init__(\n", + " self,\n", + " symbol: str,\n", + " *,\n", + " cache_spec: Optional[CacheSpec] = None,\n", + " enable_namespacing: bool = False,\n", + " ) -> None:\n", + " \"\"\"Initializes manager once per symbol instance.\"\"\"\n", + " if getattr(self, \"_initialized\", False):\n", + " return\n", + "\n", + " self._initialized = True\n", + " super().__init__(cache_spec=cache_spec, enable_namespacing=enable_namespacing)\n", + " self.symbol = symbol\n", + "\n", + " def _normalize_inputs(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " maturity_date: Union[datetime, str],\n", + " div_type: Optional[DivType],\n", + " ) -> Tuple[DivType, date, date, date, str, str, str]:\n", + " \"\"\"Converts date inputs to both date objects and strings.\"\"\"\n", + " div_type = DivType(div_type) if div_type is not None else DivType.DISCRETE\n", + "\n", + " start_dt = (\n", + " datetime.strptime(start_date, \"%Y-%m-%d\") if isinstance(start_date, str) else start_date\n", + " )\n", + " end_dt = datetime.strptime(end_date, \"%Y-%m-%d\") if isinstance(end_date, str) else end_date\n", + " mat_dt = (\n", + " datetime.strptime(maturity_date, \"%Y-%m-%d\")\n", + " if isinstance(maturity_date, str)\n", + " else maturity_date\n", + " )\n", + "\n", + " start_str = datetime.strftime(start_dt, \"%Y-%m-%d\")\n", + " end_str = datetime.strftime(end_dt, \"%Y-%m-%d\")\n", + " mat_str = datetime.strftime(mat_dt, \"%Y-%m-%d\")\n", + " return div_type, start_dt, end_dt, mat_dt, start_str, end_str, mat_str\n", + "\n", + " def _build_key(self, *, mat_str: str, div_type: DivType, use_chain_spot: bool) -> str:\n", + " \"\"\"Constructs cache key from maturity, dividend type, and spot type.\"\"\"\n", + " return self.make_key(\n", + " symbol=self.symbol,\n", + " artifact_type=ArtifactType.FWD,\n", + " series_id=SeriesId.HIST,\n", + " maturity=mat_str,\n", + " div_type=div_type.value,\n", + " use_chain_spot=use_chain_spot,\n", + " interval=Interval.EOD,\n", + " )\n", + "\n", + " def _try_get_cached(\n", + " self,\n", + " *,\n", + " key: str,\n", + " start_str: str,\n", + " end_str: str,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " div_type: DivType,\n", + " ) -> Tuple[Optional[pd.Series], bool, str, str, Optional[ForwardResult]]:\n", + " \"\"\"Checks cache for existing data and identifies missing dates.\"\"\"\n", + " cached_series = self.get(key, default=None)\n", + " if cached_series is None:\n", + " return None, False, start_str, end_str, None\n", + "\n", + " missing = get_missing_dates(cached_series, _start=start_str, _end=end_str)\n", + " if not missing:\n", + " logger.info(f\"Cache hit for forward timeseries key: {key}\")\n", + " cached_series = _data_structure_sanitize(\n", + " cached_series,\n", + " start=start_str,\n", + " end=end_str,\n", + " )\n", + "\n", + " result = ForwardResult()\n", + " if div_type == DivType.DISCRETE:\n", + " result.daily_discrete_forward = cached_series\n", + " else:\n", + " result.daily_continuous_forward = cached_series\n", + " result.dividend_type = div_type\n", + " result.key = key\n", + " return cached_series, False, start_str, end_str, result\n", + "\n", + " logger.info(\n", + " f\"Cache partially covers requested date range for forward timeseries. \"\n", + " f\"Key: {key}. Fetching missing dates: {missing}\"\n", + " )\n", + " return cached_series, True, min(missing), max(missing), None\n", + "\n", + " def _get_dividend_result(\n", + " self,\n", + " *,\n", + " start_str: str,\n", + " end_str: str,\n", + " mat_str: str,\n", + " div_type: DivType,\n", + " dividend_result: Optional[DividendsResult],\n", + " use_chain_spot: bool,\n", + " ) -> DividendsResult:\n", + " \"\"\"Fetches or validates dividend data with adjustment consistency checks.\"\"\"\n", + " if dividend_result is None:\n", + " dividend_result = DividendDataManager(symbol=self.symbol).get_schedule_timeseries(\n", + " start_date=start_str,\n", + " end_date=end_str,\n", + " maturity_date=mat_str,\n", + " div_type=div_type,\n", + " undo_adjust=use_chain_spot, # If using chain spot, back adjust dividends\n", + " )\n", + "\n", + " if dividend_result.is_empty():\n", + " raise ValueError(\"Dividend result is empty. Cannot compute forward prices without dividend information.\")\n", + "\n", + " if dividend_result.undo_adjust != use_chain_spot:\n", + " raise ValueError(\"Mismatch between dividend_result.undo_adjust and use_chain_spot. They must be the same.\")\n", + "\n", + " return dividend_result\n", + "\n", + " def _load_spot(self, *, use_chain_spot: bool, spot: Optional[TimeseriesData] = None) -> pd.Series:\n", + " \"\"\"Loads spot or chain_spot price series.\"\"\"\n", + " if spot is None:\n", + " spot = TS.get_timeseries(self.symbol, skip_preload_check=True)\n", + " if use_chain_spot:\n", + " return spot.chain_spot[\"close\"]\n", + " return spot.spot[\"close\"]\n", + "\n", + " def _load_rates(self, *, start_str: str, end_str: str, rates: Optional[RatesResult] = None) -> pd.Series:\n", + " \"\"\"Loads risk-free rates for date range.\"\"\"\n", + " if rates is None:\n", + " rates_data = RatesDataManager().get_risk_free_rate_timeseries(\n", + " start_date=start_str,\n", + " end_date=end_str,\n", + " interval=Interval.EOD,\n", + " )\n", + " rates = rates_data.daily_risk_free_rates\n", + " else:\n", + " rates = rates.daily_risk_free_rates\n", + " rates = rates[(rates.index >= pd.to_datetime(start_str)) & (rates.index <= pd.to_datetime(end_str))]\n", + " return rates\n", + "\n", + " def _align_3(\n", + " self, spot: pd.Series, rates: pd.Series, third: pd.Series, *, third_name: str\n", + " ) -> Tuple[pd.Series, pd.Series, pd.Series]:\n", + " \"\"\"Aligns three series to common dates and validates no NaNs.\"\"\"\n", + " idx = spot.index.intersection(rates.index).intersection(third.index)\n", + "\n", + " spot = spot.reindex(idx)\n", + " rates = rates.reindex(idx)\n", + " third = third.reindex(idx)\n", + "\n", + " if rates.isna().any():\n", + " raise ValueError(\"NaNs in rates after alignment.\")\n", + " if third.isna().any():\n", + " raise ValueError(f\"NaNs in {third_name} after alignment.\")\n", + "\n", + " return spot, rates, third\n", + "\n", + " def _compute_forward_discrete(\n", + " self,\n", + " *,\n", + " spot: pd.Series,\n", + " rates: pd.Series,\n", + " discrete_divs: pd.Series, # series of Schedule objects\n", + " mat_dt: date,\n", + " ) -> pd.Series:\n", + " \"\"\"Computes forward prices using discrete dividend schedules.\"\"\"\n", + "\n", + " \n", + " pv_divs = vectorized_discrete_pv(\n", + " schedules=discrete_divs.to_list(),\n", + " r=rates.tolist(),\n", + " _valuation_dates=discrete_divs.index.tolist(),\n", + " _end_dates=[mat_dt] * len(discrete_divs),\n", + " )\n", + " pv_divs = [pv_divs] if isinstance(pv_divs, (int, float)) else pv_divs \n", + "\n", + " second_vector = [(mat_dt - val).days * SECONDS_IN_DAY for val in discrete_divs.index]\n", + " t = [val / SECONDS_IN_YEAR for val in second_vector]\n", + "\n", + "\n", + " forwards = vectorized_forward_discrete(\n", + " S=spot.tolist(),\n", + " r=rates.tolist(),\n", + " T=t,\n", + " pv_divs=pv_divs,\n", + " )\n", + " return pd.Series(data=forwards, index=discrete_divs.index)\n", + "\n", + " def _compute_forward_continuous(\n", + " self,\n", + " *,\n", + " spot: pd.Series,\n", + " rates: pd.Series,\n", + " continuous_divs: pd.Series, # series of dividend yields\n", + " mat_dt: date,\n", + " ) -> pd.Series:\n", + " \"\"\"Computes forward prices using continuous dividend yields.\"\"\"\n", + " q_factor = get_vectorized_continuous_dividends(\n", + " div_rates=continuous_divs.tolist(),\n", + " _valuation_dates=continuous_divs.index.tolist(),\n", + " _end_dates=[mat_dt] * len(continuous_divs),\n", + " )\n", + "\n", + " second_vector = [(mat_dt - val).days * SECONDS_IN_DAY for val in continuous_divs.index]\n", + " t = [val / SECONDS_IN_YEAR for val in second_vector]\n", + "\n", + " forwards = vectorized_forward_continuous(\n", + " S=spot.tolist(),\n", + " r=rates.tolist(),\n", + " T=t,\n", + " q_factor=q_factor,\n", + " )\n", + " return pd.Series(data=forwards, index=continuous_divs.index)\n", + "\n", + " def _merge_partial(self, cached_series: pd.Series, forward_series: pd.Series) -> pd.Series:\n", + " \"\"\"Merges newly computed data with cached data, keeping latest values.\"\"\"\n", + " merged = pd.concat([cached_series, forward_series])\n", + " forward_series = merged[~merged.index.duplicated(keep=\"last\")]\n", + " return forward_series\n", + "\n", + "\n", + "\n", + " def get_forward_timeseries(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " maturity_date: Union[datetime, str],\n", + " div_type: Optional[DivType] = None,\n", + " spot: Optional[TimeseriesData] = None,\n", + " rates: Optional[RatesResult] = None,\n", + " *,\n", + " dividend_result: Optional[DividendsResult] = None,\n", + " use_chain_spot: bool = True,\n", + " ) -> ForwardResult:\n", + " \"\"\"\n", + " Returns a DAILY series (indexed by date) where each value is the forward price\n", + " from that valuation date up to maturity_date.\n", + "\n", + " - start_date/end_date define the valuation date range\n", + " - maturity_date is the fixed horizon (e.g., option expiry)\n", + " \"\"\"\n", + " result = ForwardResult()\n", + " og_start_date = start_date\n", + " og_end_date = end_date\n", + " div_type, start_dt, end_dt, mat_dt, start_str, end_str, mat_str = self._normalize_inputs(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " maturity_date=maturity_date,\n", + " div_type=div_type,\n", + " )\n", + "\n", + " if mat_dt < start_dt:\n", + " raise ValueError(\"maturity_date must be >= start_date\")\n", + "\n", + " key = self._build_key(mat_str=mat_str, div_type=div_type, use_chain_spot=use_chain_spot)\n", + "\n", + " cached_series, partial_hit, start_str, end_str, cached_result = self._try_get_cached(\n", + " key=key,\n", + " start_str=start_str,\n", + " end_str=end_str,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " div_type=div_type,\n", + " )\n", + " if cached_result is not None:\n", + " return cached_result\n", + " \n", + " dividend_result = self._get_dividend_result(\n", + " start_str=start_str,\n", + " end_str=end_str,\n", + " mat_str=mat_str,\n", + " div_type=div_type,\n", + " dividend_result=dividend_result,\n", + " use_chain_spot=use_chain_spot,\n", + " )\n", + "\n", + " spot = self._load_spot(use_chain_spot=use_chain_spot, spot=spot) \n", + " rates = self._load_rates(start_str=start_str, end_str=end_str, rates=rates)\n", + "\n", + " if div_type == DivType.DISCRETE:\n", + " discrete_divs = dividend_result.daily_discrete_dividends\n", + "\n", + " spot, rates, discrete_divs = self._align_3(\n", + " spot=spot,\n", + " rates=rates,\n", + " third=discrete_divs,\n", + " third_name=\"discrete dividend schedules\",\n", + " )\n", + "\n", + " forward_series = self._compute_forward_discrete(\n", + " spot=spot,\n", + " rates=rates,\n", + " discrete_divs=discrete_divs,\n", + " mat_dt=mat_dt,\n", + " )\n", + "\n", + " result.daily_discrete_forward = forward_series\n", + " result.dividend_result = dividend_result\n", + "\n", + " elif div_type == DivType.CONTINUOUS:\n", + " continuous_divs = dividend_result.daily_continuous_dividends\n", + "\n", + " spot, rates, continuous_divs = self._align_3(\n", + " spot=spot,\n", + " rates=rates,\n", + " third=continuous_divs,\n", + " third_name=\"div yields\",\n", + " )\n", + "\n", + " forward_series = self._compute_forward_continuous(\n", + " spot=spot,\n", + " rates=rates,\n", + " continuous_divs=continuous_divs,\n", + " mat_dt=mat_dt,\n", + " )\n", + "\n", + " result.daily_continuous_forward = forward_series\n", + " result.dividend_result = dividend_result\n", + "\n", + " else:\n", + " raise ValueError(f\"Unsupported dividend type: {div_type}\")\n", + "\n", + " result.dividend_type = div_type\n", + " result.key = key\n", + "\n", + " if partial_hit:\n", + " forward_series = self._merge_partial(cached_series=cached_series, forward_series=forward_series)\n", + "\n", + " self.cache_it(key, forward_series, expire=86400 / 2) # 12 hours expiry\n", + "\n", + " forward_series = _data_structure_sanitize(\n", + " forward_series,\n", + " start=og_start_date,\n", + " end=og_end_date,\n", + " )\n", + "\n", + " if div_type == DivType.DISCRETE:\n", + " result.daily_discrete_forward = forward_series\n", + " else:\n", + " result.daily_continuous_forward = forward_series\n", + "\n", + " return result\n", + "\n", + " def make_key(self, *, symbol, interval=None, artifact_type=None, series_id=None, **extra_parts):\n", + " \"\"\"Delegates to BaseDataManager key construction.\"\"\"\n", + " return super().make_key(\n", + " symbol=symbol, interval=interval, artifact_type=artifact_type, series_id=series_id, **extra_parts\n", + " )\n", + "\n", + " def cache_it(self, key, value, *, expire=None):\n", + " \"\"\"Merges and caches forward timeseries, excluding today's partial data.\"\"\"\n", + " ## Since it is a timeseries, we will append to existing if exists\n", + " _data_structure_cache_it(self, key, value, expire=expire)\n", + " return\n", + "\n", + "\n", + " def get_forward(self, \n", + " date: Union[datetime, str], \n", + " maturity_date: Union[datetime, str],\n", + " div_type: Optional[DivType] = None,\n", + " dividend_result: Optional[DividendsResult] = None,\n", + " spot: Optional[TimeseriesData] = None,\n", + " rates: Optional[RatesResult] = None,\n", + " *, \n", + " use_chain_spot: bool = True) -> ForwardResult:\n", + " \"\"\"\n", + " Returns the forward price at a specific valuation datetime\n", + " div_type = DivType(div_type) if div_type is not None else DivType.DISCRETE\n", + " \"\"\"\n", + " div_type = DivType(div_type) if div_type is not None else DivType.DISCRETE\n", + " date_str = date.strftime(\"%Y-%m-%d\") if isinstance(date, datetime) else date\n", + " mat_str = maturity_date.strftime(\"%Y-%m-%d\") if isinstance(maturity_date, datetime) else maturity_date\n", + " start = date_str\n", + " end = date_str\n", + "\n", + " result = self.get_forward_timeseries(\n", + " start_date=start,\n", + " end_date=end,\n", + " maturity_date=mat_str,\n", + " div_type=div_type,\n", + " use_chain_spot=use_chain_spot,\n", + " dividend_result=dividend_result,\n", + " spot=spot,\n", + " rates=rates,\n", + " )\n", + " return result\n", + "\n", + " \n", + "\n", + " def offload(self, *args: Any, **kwargs: Any) -> None:\n", + " \"\"\"\n", + " Example implementation of offload for ForwardDataManager.\n", + " \"\"\"\n", + " print(f\"No offload logic implemented for {self.CACHE_NAME}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "6810ea3e", + "metadata": {}, + "outputs": [], + "source": [ + "fwd_test = ForwardDataManager(symbol=\"COST\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "08eb286b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:37 [test] __main__ INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "2026-01-18 19:43:37 [test] __main__ INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "Sanitizing data from 2026-01-10 to 2026-01-14...\n", + "2026-01-18 19:43:37 [test] __main__ INFO: Cache hit for forward timeseries key: symbol:COST|interval:eod|artifact_type:forward|series_id:hist|div_type:DISCRETE|maturity:2026-01-20|use_chain_spot:0\n", + "Sanitizing data from 2026-01-10 to 2026-01-14...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-12 943.810580\n", + "2026-01-13 942.573305\n", + "2026-01-14 951.536662\n", + "dtype: float64" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "fwd_discrete = fwd_test.get_forward_timeseries(\n", + " start_date=\"2026-01-10\",\n", + " end_date=\"2026-01-14\",\n", + " maturity_date=\"2026-01-20\",\n", + " div_type=DivType.DISCRETE,\n", + " use_chain_spot=False,\n", + " spot=TS.get_timeseries(\"COST\", skip_preload_check=True),\n", + " rates=RatesDataManager().get_risk_free_rate_timeseries(\n", + " start_date=\"2026-01-10\",\n", + " end_date=\"2026-01-14\",\n", + " ),\n", + ")\n", + "\n", + "# fwd_cont = fwd_test.get_forward_timeseries(\n", + "# start_date=\"2025-01-02\",\n", + "# end_date=\"2026-01-15\",\n", + "# maturity_date=\"2026-01-02\",\n", + "# div_type=DivType.CONTINUOUS,\n", + "# use_chain_spot=False,\n", + "# )\n", + "fwd_discrete.daily_discrete_forward" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "6ce9318a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:38 [test] __main__ INFO: Cache hit for key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-01-18 19:43:38 [test] __main__ INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-01-18 19:43:38 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker AAPL\n", + "2026-01-18 19:43:38 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 12, for original valuation: 4. Size from historical divs: 8\n", + "2026-01-18 19:43:38 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 4\n", + "2026-01-18 19:43:38 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.26, 0.26, 0.26, 0.26]\n", + "2026-01-18 19:43:38 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.24, 0.25, 0.25, 0.25, 0.25, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26]\n", + "2026-01-18 19:43:38 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2024, 2, 9), datetime.date(2024, 5, 10), datetime.date(2024, 8, 12), datetime.date(2024, 11, 8), datetime.date(2025, 2, 10), datetime.date(2025, 5, 12), datetime.date(2025, 8, 11), datetime.date(2025, 11, 10), datetime.date(2026, 2, 10), datetime.date(2026, 5, 10), datetime.date(2026, 8, 10), datetime.date(2026, 11, 10)]\n", + "2026-01-18 19:43:38 [test] __main__ INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "2026-01-18 19:43:38 [test] __main__ INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "Sanitizing data from 2026-01-14 to 2026-01-14...\n", + "2026-01-18 19:43:38 [test] __main__ INFO: Cache hit for forward timeseries key: symbol:COST|interval:eod|artifact_type:forward|series_id:hist|div_type:DISCRETE|maturity:2026-01-20|use_chain_spot:0\n", + "Sanitizing data from 2026-01-14 to 2026-01-14...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-14 951.536662\n", + "dtype: float64" + ] + }, + "execution_count": 72, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "div = testdiv.get_schedule(\n", + " valuation_date=\"2026-01-14\",\n", + " maturity_date=\"2027-01-02\",\n", + " div_type=DivType.DISCRETE,\n", + " undo_adjust=False,\n", + ")\n", + "\n", + "rate = rt_manager.get_rate(\n", + " date=\"2026-01-14\",\n", + ")\n", + "\n", + "spot = TS.get_timeseries(\"AAPL\", skip_preload_check=True, start_date=\"2026-01-14\", end_date=\"2026-01-14\")\n", + "\n", + "fwd_test.get_forward(\n", + " date=\"2026-01-14\",\n", + " maturity_date=\"2026-01-20\",\n", + " div_type=DivType.DISCRETE,\n", + " use_chain_spot=False,\n", + " dividend_result=div,\n", + " spot=spot,\n", + " rates=rate\n", + ").daily_discrete_forward" + ] + }, + { + "cell_type": "markdown", + "id": "62a58879", + "metadata": {}, + "source": [ + "## Equity Market Timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "20d7e17c", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class SpotResult(Result):\n", + " \"\"\"Contains spot price data with optional split adjustment information.\"\"\"\n", + " daily_spot: Optional[pd.Series] = None\n", + " undo_adjust: Optional[bool] = None\n", + " key: Optional[str] = None\n", + "\n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides spot-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"key\": self.key,\n", + " \"is_empty\": self.daily_spot is None or self.daily_spot.empty,\n", + " \"undo_adjust\": self.undo_adjust,\n", + " }\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "6b38a71c", + "metadata": {}, + "outputs": [], + "source": [ + "class SpotDataManager(BaseDataManager):\n", + " \"\"\"Manages spot price retrieval for a specific symbol with split adjustment support.\"\"\"\n", + " CACHE_NAME: ClassVar[str] = \"spot_data_manager\"\n", + " DEFAULT_SERIES_ID: ClassVar[\"SeriesId\"] = SeriesId.HIST\n", + " INSTANCES = {}\n", + " def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> \"SpotDataManager\":\n", + " \"\"\"Returns cached instance for symbol, creating new one if needed.\"\"\"\n", + " if symbol not in cls.INSTANCES:\n", + " TS.load_timeseries(symbol, start_date=OPTION_TIMESERIES_START_DATE, end_date=datetime.now())\n", + " instance = super(SpotDataManager, cls).__new__(cls)\n", + " cls.INSTANCES[symbol] = instance\n", + " return cls.INSTANCES[symbol]\n", + " \n", + " def __init__(self, symbol: str, *, cache_spec: Optional[CacheSpec] = None, enable_namespacing: bool = False) -> None:\n", + " \"\"\"Initializes manager once per symbol instance.\"\"\"\n", + " if getattr(self, \"_initialized\", False):\n", + " return\n", + " super().__init__(cache_spec=cache_spec, enable_namespacing=enable_namespacing)\n", + " self.symbol = symbol\n", + "\n", + " def get_spot_timeseries(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " undo_adjust: bool = True,\n", + " ) -> SpotResult:\n", + " \"\"\"Returns spot or chain_spot price series for date range from MarketTimeseries.\"\"\"\n", + " \n", + " timeseries = TS.get_timeseries(self.symbol, skip_preload_check=True, start_date=start_date, end_date=end_date)\n", + " if undo_adjust:\n", + " spot_series = timeseries.chain_spot[\"close\"]\n", + " else:\n", + " spot_series = timeseries.spot[\"close\"]\n", + " \n", + " spot_series = _data_structure_sanitize(\n", + " spot_series,\n", + " start=start_date,\n", + " end=end_date,\n", + " )\n", + " result = SpotResult()\n", + " key = None # No caching key for now\n", + " result.daily_spot = spot_series\n", + " result.undo_adjust = undo_adjust\n", + " result.key = key\n", + " return result\n", + "\n", + " def get_at_time(\n", + " self,\n", + " date: Union[datetime, str],\n", + " ) -> AtIndexResult:\n", + " \"\"\"Returns spot data at a specific datetime from MarketTimeseries.\"\"\"\n", + " \n", + " return TS.get_at_index(sym=self.symbol, index=date)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "aa4e6e3f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sanitizing data from 2026-01-10 to 2026-01-14...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-12 260.250000\n", + "2026-01-13 261.049988\n", + "2026-01-14 259.959991\n", + "Name: close, dtype: float64" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_spot = SpotDataManager(symbol=\"AAPL\")\n", + "spot_result = test_spot.get_spot_timeseries(\n", + " start_date=\"2026-01-10\",\n", + " end_date=\"2026-01-14\",\n", + " undo_adjust=True,\n", + ")\n", + "spot_result.daily_spot" + ] + }, + { + "cell_type": "markdown", + "id": "b7f7a86d", + "metadata": {}, + "source": [ + "## Option Spot" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "f247fc2b", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "@dataclass\n", + "class OptionSpotResult(Result):\n", + " \"\"\"Container for option spot price timeseries data.\"\"\"\n", + " daily_option_spot: Optional[pd.DataFrame] = None\n", + " key: Optional[str] = None\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None\n", + "\n", + " @property\n", + " def close(self) -> pd.Series:\n", + " if not self.is_empty():\n", + " return self.daily_option_spot[\"close\"]\n", + " else:\n", + " return pd.Series(name=\"close\", index=pd.DatetimeIndex([]), dtype=float)\n", + " \n", + "\n", + " @property\n", + " def midpoint(self) -> pd.Series:\n", + " if not self.is_empty():\n", + " return self.daily_option_spot[\"midpoint\"]\n", + " else:\n", + " return pd.Series(name=\"midpoint\", index=pd.DatetimeIndex([]), dtype=float)\n", + " \n", + " def is_empty(self) -> bool:\n", + " \"\"\"Checks if option spot data is missing or empty.\"\"\"\n", + " return self.daily_option_spot is None or self.daily_option_spot.empty\n", + " \n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides metadata on data presence.\"\"\"\n", + " return {\n", + " \"key\": self.key,\n", + " \"is_empty\": self.is_empty(),\n", + " \"endpoint_source\": self.endpoint_source,\n", + " }\n", + " \n", + " def __repr__(self) -> str:\n", + " \"\"\"Delegates to base Result repr.\"\"\"\n", + " return super().__repr__()\n", + " \n", + "\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "e9588a19", + "metadata": {}, + "outputs": [], + "source": [ + "class OptionSpotDataManager(BaseDataManager):\n", + " \"\"\"Manages option spot price retrieval for a specific symbol from Thetadata API.\"\"\"\n", + " CACHE_NAME: str = \"option_spot_manager\"\n", + " DEFAULT_SERIES_ID: str = SeriesId.HIST\n", + " CONFIG = OptionDataConfig()\n", + " INSTANCES = {}\n", + "\n", + " def __init__(\n", + " self,\n", + " symbol: str,\n", + " *,\n", + " cache_spec: Optional[CacheSpec] = None,\n", + " enable_namespacing: bool = False,\n", + " ) -> None:\n", + " \"\"\"Initializes manager for a specific symbol.\"\"\"\n", + " super().__init__(cache_spec=cache_spec, enable_namespacing=enable_namespacing)\n", + " self.symbol = symbol\n", + "\n", + " def _sync_date(\n", + " self,\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " strike: Optional[float] = None,\n", + " expiration: Optional[Union[datetime, str]] = None,\n", + " right: Optional[str] = None,\n", + " ) -> Tuple[DATE_HINT, DATE_HINT]:\n", + " \n", + " \"\"\"\"\"\"\n", + "\n", + " dates = list_dates(\n", + " symbol=self.symbol,\n", + " exp=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " )\n", + "\n", + " dates = to_datetime(dates)\n", + " min_date, max_date = min(dates), max(dates)\n", + " start_date = max(min_date, start_date)\n", + " end_date = min(end_date, max_date)\n", + "\n", + " return start_date, end_date\n", + " \n", + " def get_option_spot(\n", + " self,\n", + " date: Union[datetime, str],\n", + " *,\n", + " strike: Optional[float] = None,\n", + " expiration: Optional[Union[datetime, str]] = None,\n", + " right: Optional[str] = None,\n", + " opttick: Optional[str] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " ) -> OptionSpotResult:\n", + " \"\"\"Fetches option spot price for a single date from Thetadata API.\"\"\"\n", + " date_str = pd.to_datetime(date).strftime(\"%Y-%m-%d\") if isinstance(date, datetime) else date\n", + " result = self.get_option_spot_timeseries(\n", + " start_date=date_str,\n", + " end_date=date_str,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " opttick=opttick,\n", + " endpoint_source=endpoint_source,\n", + " )\n", + " return result\n", + "\n", + " def get_option_spot_timeseries(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " *,\n", + " strike: Optional[float] = None,\n", + " expiration: Optional[Union[datetime, str]] = None,\n", + " right: Optional[str] = None,\n", + " opttick: Optional[str] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " ) -> OptionSpotResult:\n", + " \"\"\"Fetches option spot price timeseries from Thetadata API.\"\"\"\n", + " if endpoint_source is None:\n", + " endpoint_source = self.CONFIG.option_spot_endpoint_source\n", + " \n", + " strike, right, symbol, expiration = _handle_opttick_param(\n", + " strike=strike,\n", + " right=right,\n", + " symbol=self.symbol,\n", + " exp=expiration,\n", + " opttick=opttick,\n", + " )\n", + "\n", + " date_packet = DateRangePacket(start_date=start_date, end_date=end_date)\n", + " start_date, end_date = date_packet.start_date, date_packet.end_date\n", + " start_date, end_date = self._sync_date(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " strike=float(strike),\n", + " expiration=expiration,\n", + " right=right,\n", + " )\n", + " start_str, end_str = date_packet.start_str, date_packet.end_str\n", + " \n", + " # Construct cache key\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " artifact_type=ArtifactType.OPTION_SPOT,\n", + " series_id=SeriesId.HIST,\n", + " endpoint_source=endpoint_source.value,\n", + " interval=Interval.EOD,\n", + " strike=strike,\n", + " right=right,\n", + " expiration=expiration,\n", + " )\n", + "\n", + "\n", + " \n", + " # Check cache\n", + " cached_data, is_partial, start_date, end_date = _check_cache_for_timeseries_data_structure(\n", + " key=key,\n", + " self=self,\n", + " start_dt=start_date,\n", + " end_dt=end_date,\n", + " )\n", + "\n", + " if cached_data is not None and not is_partial:\n", + " logger.info(f\"Cache hit for option spot timeseries key: {key}\")\n", + " result = OptionSpotResult()\n", + " result.daily_option_spot = cached_data\n", + " result.key = key\n", + " result.endpoint_source = endpoint_source\n", + " return result\n", + " elif is_partial:\n", + " logger.info(f\"Cache partially covers requested date range for option spot timeseries. Key: {key}. Fetching missing dates.\")\n", + " else:\n", + " logger.info(f\"No cache found for option spot timeseries key: {key}. Fetching from source.\")\n", + " \n", + " # Fetch data from Thetadata API (placeholder logic)\n", + " fetched_data = self._query_thetadata_api(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " endpoint_source=endpoint_source,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " )\n", + " \n", + " # Merge with cached data if partial\n", + " if cached_data is not None and is_partial:\n", + " merged = pd.concat([cached_data, fetched_data])\n", + " fetched_data = merged[~merged.index.duplicated(keep='last')]\n", + "\n", + " fetched_data.index = default_timestamp(fetched_data.index)\n", + "\n", + " # Cache the fetched data\n", + " _data_structure_cache_it(self, key, fetched_data) # 24 hours expiry\n", + "\n", + " # Sanitize before returning\n", + " fetched_data = _data_structure_sanitize(\n", + " fetched_data,\n", + " start=start_str,\n", + " end=end_str,\n", + " )\n", + " \n", + " result = OptionSpotResult()\n", + " result.daily_option_spot = fetched_data\n", + " result.key = key\n", + " result.endpoint_source = endpoint_source\n", + " return result\n", + "\n", + "\n", + " def _query_thetadata_api(\n", + " self,\n", + " start_date: Union[datetime, str],\n", + " end_date: Union[datetime, str],\n", + " endpoint_source: OptionSpotEndpointSource,\n", + " strike: Optional[float] = None,\n", + " expiration: Optional[Union[datetime, str]] = None,\n", + " right: Optional[str] = None,\n", + " ) -> pd.DataFrame:\n", + " \"\"\"Placeholder method to simulate fetching option spot data from Thetadata API.\"\"\"\n", + " # In a real implementation, this method would make HTTP requests to Thetadata's API.\n", + " if endpoint_source == OptionSpotEndpointSource.EOD:\n", + " return retrieve_eod_ohlc(\n", + " symbol=self.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " strike=float(strike),\n", + " exp=expiration,\n", + " right=right,\n", + " )\n", + "\n", + " else:\n", + " logger.info(f\"Fetching option spot data from Thetadata Quote endpoint for {self.symbol} from {start_date} to {end_date}.\")\n", + " return quote_to_eod_patch(\n", + " symbol=self.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " strike=float(strike),\n", + " exp=expiration,\n", + " right=right,\n", + " ohlc_format=True,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "93e3759a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:39 [test] __main__ INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:2028-03-17|right:C|strike:200\n", + "Sanitizing data from 2026-01-10 00:00:00 to 2026-01-14 00:00:00...\n", + "2026-01-18 19:43:39 [test] __main__ INFO: Cache hit for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:2028-03-17|right:C|strike:200\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openhighlowclosevolumebid_sizeclosebidask_sizecloseaskmidpointweighted_midpoint
datetime
2026-01-120.088.2250.087.200NaN3186.802987.6087.20087.186667
2026-01-130.088.2000.087.850NaN787.351288.3587.85087.981579
2026-01-140.088.3000.087.125NaN1786.551687.7087.12587.107576
\n", + "
" + ], + "text/plain": [ + " open high low close volume bid_size closebid ask_size \\\n", + "datetime \n", + "2026-01-12 0.0 88.225 0.0 87.200 NaN 31 86.80 29 \n", + "2026-01-13 0.0 88.200 0.0 87.850 NaN 7 87.35 12 \n", + "2026-01-14 0.0 88.300 0.0 87.125 NaN 17 86.55 16 \n", + "\n", + " closeask midpoint weighted_midpoint \n", + "datetime \n", + "2026-01-12 87.60 87.200 87.186667 \n", + "2026-01-13 88.35 87.850 87.981579 \n", + "2026-01-14 87.70 87.125 87.107576 " + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spot_option_manager = OptionSpotDataManager(symbol=\"AAPL\")\n", + "data = spot_option_manager.get_option_spot_timeseries(\n", + " start_date=\"2026-01-10\",\n", + " end_date=\"2026-01-14\",\n", + " endpoint_source=OptionSpotEndpointSource.QUOTE,\n", + " strike=200,\n", + " expiration=\"2028-03-17\",\n", + " right=\"C\",\n", + ")\n", + "data.daily_option_spot" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "df2ff7b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:39 [test] __main__ INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:2028-03-17|right:C|strike:200\n", + "Sanitizing data from 2026-01-12 00:00:00 to 2026-01-12 00:00:00...\n", + "2026-01-18 19:43:39 [test] __main__ INFO: Cache hit for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:2028-03-17|right:C|strike:200\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-12 87.2\n", + "Name: midpoint, dtype: float64" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spot_option_manager = OptionSpotDataManager(symbol=\"AAPL\")\n", + "data = spot_option_manager.get_option_spot(\n", + " date=\"2026-01-12\",\n", + " endpoint_source=OptionSpotEndpointSource.EOD,\n", + " strike=200,\n", + " expiration=\"2028-03-17\",\n", + " right=\"C\",\n", + ")\n", + "data.midpoint" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f62e8454", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
rootexpirationstrikeright
2874AAPL20260116200.0C
2875AAPL20260116200.0P
2960AAPL20260123200.0P
2962AAPL20260123200.0C
2880AAPL20260130200.0P
2879AAPL20260130200.0C
2965AAPL20260206200.0P
2966AAPL20260206200.0C
2888AAPL20260213200.0C
2887AAPL20260213200.0P
2969AAPL20260220200.0P
2971AAPL20260220200.0C
2891AAPL20260227200.0C
2892AAPL20260227200.0P
2974AAPL20260320200.0P
2975AAPL20260320200.0C
2978AAPL20260417200.0C
2979AAPL20260417200.0P
2982AAPL20260515200.0C
2983AAPL20260515200.0P
2985AAPL20260618200.0C
2984AAPL20260618200.0P
2910AAPL20260717200.0C
2911AAPL20260717200.0P
2990AAPL20260821200.0P
2991AAPL20260821200.0C
2992AAPL20260918200.0P
2993AAPL20260918200.0C
2917AAPL20261120200.0P
2916AAPL20261120200.0C
2926AAPL20261218200.0C
2924AAPL20261218200.0P
2935AAPL20270115200.0P
2937AAPL20270115200.0C
3012AAPL20270617200.0P
3013AAPL20270617200.0C
2972AAPL20271217200.0P
2973AAPL20271217200.0C
3028AAPL20280121200.0C
3029AAPL20280121200.0P
0AAPL20280317200.0C
1AAPL20280317200.0P
33AAPL20281215200.0P
35AAPL20281215200.0C
\n", + "
" + ], + "text/plain": [ + " root expiration strike right\n", + "2874 AAPL 20260116 200.0 C\n", + "2875 AAPL 20260116 200.0 P\n", + "2960 AAPL 20260123 200.0 P\n", + "2962 AAPL 20260123 200.0 C\n", + "2880 AAPL 20260130 200.0 P\n", + "2879 AAPL 20260130 200.0 C\n", + "2965 AAPL 20260206 200.0 P\n", + "2966 AAPL 20260206 200.0 C\n", + "2888 AAPL 20260213 200.0 C\n", + "2887 AAPL 20260213 200.0 P\n", + "2969 AAPL 20260220 200.0 P\n", + "2971 AAPL 20260220 200.0 C\n", + "2891 AAPL 20260227 200.0 C\n", + "2892 AAPL 20260227 200.0 P\n", + "2974 AAPL 20260320 200.0 P\n", + "2975 AAPL 20260320 200.0 C\n", + "2978 AAPL 20260417 200.0 C\n", + "2979 AAPL 20260417 200.0 P\n", + "2982 AAPL 20260515 200.0 C\n", + "2983 AAPL 20260515 200.0 P\n", + "2985 AAPL 20260618 200.0 C\n", + "2984 AAPL 20260618 200.0 P\n", + "2910 AAPL 20260717 200.0 C\n", + "2911 AAPL 20260717 200.0 P\n", + "2990 AAPL 20260821 200.0 P\n", + "2991 AAPL 20260821 200.0 C\n", + "2992 AAPL 20260918 200.0 P\n", + "2993 AAPL 20260918 200.0 C\n", + "2917 AAPL 20261120 200.0 P\n", + "2916 AAPL 20261120 200.0 C\n", + "2926 AAPL 20261218 200.0 C\n", + "2924 AAPL 20261218 200.0 P\n", + "2935 AAPL 20270115 200.0 P\n", + "2937 AAPL 20270115 200.0 C\n", + "3012 AAPL 20270617 200.0 P\n", + "3013 AAPL 20270617 200.0 C\n", + "2972 AAPL 20271217 200.0 P\n", + "2973 AAPL 20271217 200.0 C\n", + "3028 AAPL 20280121 200.0 C\n", + "3029 AAPL 20280121 200.0 P\n", + "0 AAPL 20280317 200.0 C\n", + "1 AAPL 20280317 200.0 P\n", + "33 AAPL 20281215 200.0 P\n", + "35 AAPL 20281215 200.0 C" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "c = list_contracts(symbol=\"AAPL\", start_date=\"2026-01-16\")\n", + "c.sort_values(by=\"expiration\").query(\"strike == 200\").head(50)" + ] + }, + { + "cell_type": "markdown", + "id": "99058897", + "metadata": {}, + "source": [ + "## Vol Manager" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "39e3701b", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class VolatilityResult(Result):\n", + " \"\"\"Contains volatility surface data.\"\"\"\n", + " timeseries: Optional[pd.Series] = None\n", + " key: Optional[str] = None\n", + "\n", + " def is_empty(self) -> bool:\n", + " \"\"\"Checks if volatility data is missing or empty.\"\"\"\n", + " return self.timeseries is None or self.timeseries.empty\n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides volatility-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"key\": self.key,\n", + " \"is_empty\": self.is_empty(),\n", + " }\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "f5968b56", + "metadata": {}, + "outputs": [], + "source": [ + "def time_distance_helper(start: datetime, end: datetime) -> float:\n", + " \"\"\"Calculates time distance in years between two dates.\"\"\"\n", + " delta = (to_datetime(end) - to_datetime(start)).days * SECONDS_IN_DAY\n", + " return delta / SECONDS_IN_YEAR" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "0e47ae05", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-18 19:43:41 [test] __main__ INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:2026-07-17|right:C|strike:200\n", + "Sanitizing data from 2025-11-20 00:00:00 to 2026-01-16 00:00:00...\n", + "2026-01-18 19:43:41 [test] __main__ INFO: Cache hit for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:2026-07-17|right:C|strike:200\n", + "2026-01-18 19:43:41 [test] __main__ INFO: Fetching discrete dividend schedule timeseries for AAPL from 2025-11-20 00:00:00 to 2026-01-16 00:00:00 with maturity 2026-07-17\n", + "2026-01-18 19:43:41 [test] __main__ INFO: Cache hit for discrete schedule timeseries key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-07-17|method:CONSTANT|undo_adjust:1\n", + "2026-01-18 19:43:41 [test] __main__ INFO: Cache fully covers requested date range for timeseries. Key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-07-17|method:CONSTANT|undo_adjust:1\n", + "2026-01-18 19:43:41 [test] __main__ INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "2026-01-18 19:43:41 [test] __main__ INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist\n", + "Sanitizing data from 2025-11-20 to 2026-01-16...\n", + "Sanitizing data from 2025-11-20 00:00:00 to 2026-01-16 00:00:00...\n" + ] + } + ], + "source": [ + "# \n", + "\n", + "ts_start = \"2025-01-01\"\n", + "ts_end = \"2026-01-18\"\n", + "expiration = \"2026-07-17\"\n", + "market_price = (\n", + " OptionSpotDataManager(symbol=\"AAPL\")\n", + " .get_option_spot_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " strike=200,\n", + " expiration=expiration,\n", + " right=\"C\",\n", + " )\n", + " .midpoint\n", + ")\n", + "dividends = DividendDataManager(symbol=\"AAPL\").get_schedule_timeseries(\n", + " start_date=market_price.index.min(),\n", + " end_date=market_price.index.max(),\n", + " maturity_date=expiration,\n", + " div_type=DivType.DISCRETE,\n", + ")\n", + "dividends_res = vector_convert_to_time_frac(\n", + " schedules=dividends.daily_discrete_dividends,\n", + " valuation_dates=dividends.daily_discrete_dividends.index.tolist(),\n", + " end_dates=[to_datetime(expiration)] * len(dividends.daily_discrete_dividends),\n", + ")\n", + "\n", + "sigma = [0.2] * len(dividends_res)\n", + "r = RatesDataManager().get_risk_free_rate_timeseries(\n", + " start_date=market_price.index.min(),\n", + " end_date=market_price.index.max(),\n", + ").daily_risk_free_rates\n", + "T = [time_distance_helper(start=dt, end=expiration) for dt in dividends.daily_discrete_dividends.index]\n", + "S0 = SpotDataManager(symbol=\"AAPL\").get_spot_timeseries(\n", + " start_date=market_price.index.min(),\n", + " end_date=market_price.index.max(),\n", + ").daily_spot\n", + "\n", + "right = [\"c\"] * len(dividends.daily_discrete_dividends)\n", + "dividend_type = [DivType.DISCRETE.value] * len(dividends.daily_discrete_dividends)\n", + "K = [200.0] * len(dividends.daily_discrete_dividends)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "id": "b503aba7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "77.34927314808915" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "i = 20\n", + "date = \"2026-01-06\"\n", + "crr_binomial_pricing(\n", + " K = K[i],\n", + " T = T[i],\n", + " sigma = sigma[i],\n", + " r = r.loc[dividends.daily_discrete_dividends.index[i]],\n", + " S0 = S0.loc[dividends.daily_discrete_dividends.index[i]],\n", + " dividend_type = dividend_type,\n", + " dividends = (dividends_res[i].schedule),\n", + " option_type = right[i],\n", + " N = 100,\n", + " american = True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "id": "afa79aa4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(0.35225717942950563)" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "estimate_crr_implied_volatility(\n", + " S=S0.loc[dividends.daily_discrete_dividends.index[i]],\n", + " K=K[i],\n", + " T=T[i],\n", + " r=r.loc[dividends.daily_discrete_dividends.index[i]],\n", + " market_price=market_price.loc[dividends.daily_discrete_dividends.index[i]],\n", + " dividend_type=dividend_type[i],\n", + " q=dividends_res[i].schedule,\n", + " option_type=right[i],\n", + " N=100,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "17bd1367", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2025-11-20 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-11-21 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-11-24 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-11-25 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-11-26 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-11-28 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-01 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-02 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-03 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-04 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-05 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-08 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-09 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-10 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-11 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-12 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-15 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-16 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-17 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-18 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-19 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-22 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-23 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-24 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-26 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-29 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-30 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2025-12-31 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-02 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-05 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-06 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-07 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-08 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-09 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-12 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-13 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-14 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-15 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "2026-01-16 ((2026-02-10, 0.26), (2026-05-10, 0.26))\n", + "Name: dividend_schedule, dtype: object" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from trade.helpers.helper import get_parrallel_apply, runProcesses, runThreads\n", + "s = slice(0, 272)\n", + "s\n", + "len(r)\n", + "r\n", + "S0\n", + "dividends.daily_discrete_dividends" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "44ae8203", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[71.02630033668731,\n", + " 76.09229303220053,\n", + " 80.37353532681382,\n", + " 81.38808716012909,\n", + " 81.96444947401316,\n", + " 83.13808721068547,\n", + " 87.27711472403652,\n", + " 90.29754247077118,\n", + " 88.20660139858977,\n", + " 84.73446831932573,\n", + " 82.80205029879087,\n", + " 81.87362207160093,\n", + " 81.16549041598502,\n", + " 82.68146918792709,\n", + " 81.88873890121882,\n", + " 82.06495491180878,\n", + " 77.88984162045772,\n", + " 78.37087626259574,\n", + " 75.61148315977978,\n", + " 75.91054322876582,\n", + " 77.35073526416885,\n", + " 74.62624448060924,\n", + " 75.99617143789405,\n", + " 77.41758579513007,\n", + " 76.9557065376728,\n", + " 77.24149177132259,\n", + " 76.54931516941679,\n", + " 75.3282815804333,\n", + " 74.4296114685626,\n", + " 70.64548361128149,\n", + " 65.81246752064467,\n", + " 63.79651442825223,\n", + " 62.50227051241733,\n", + " 62.80793623535227,\n", + " 63.61869929257374,\n", + " 64.40448392746192,\n", + " 63.31367744975112,\n", + " 61.581577922293064,\n", + " 58.94114722939313]" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vector_crr_binomial_pricing(\n", + " K[s], # K\n", + " T[s], # T\n", + " sigma[s], # sigma\n", + " r.tolist()[s], # r\n", + " ([250] * len(K))[s], # N\n", + " S0.tolist()[s], # S0\n", + " right[s], # option_type\n", + " ([True] * len(K))[s], # american\n", + " ([0.0] * len(K))[s], # dividend_yield\n", + " [dividends_res[i].schedule for i in range(len(dividends_res))][s], # dividends,\n", + " dividend_type[s],\n", ")" ] } diff --git a/trade/datamanager/notebooks/greeksdn.ipynb b/trade/datamanager/notebooks/greeksdn.ipynb new file mode 100644 index 0000000..4c82b9e --- /dev/null +++ b/trade/datamanager/notebooks/greeksdn.ipynb @@ -0,0 +1,1848 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "b443b759", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-28 19:52:22 trade.helpers.Logging INFO: Logging Root Directory: /Users/chiemelienwanisobi/cloned_repos/QuantTools/logs\n", + "2026-01-28 19:52:22 [test] trade.helpers.clear_cache INFO: No expired caches to delete on 2026-01-28.\n", + "2026-01-28 19:52:24 [test] dbase.DataAPI.ThetaData.proxy INFO: Refreshed proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-01-28 19:52:24 [test] dbase.DataAPI.ThetaData.proxy INFO: Using Proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-01-28 19:52:24 [test] dbase.DataAPI.ThetaData INFO: Using V2 of the ThetaData API\n", + "\n", + "\n", + "Scheduled Data Requests will be saved to: /Users/chiemelienwanisobi/cloned_repos/QuantTools/module_test/raw_code/DataManagers/scheduler/requests.jsonl\n", + "2026-01-28 19:52:26 [test] DataManager.py CRITICAL: Using ProcessSaveManager for saving data.\n", + "Fetching rates data from yfinance directly during market hours\n", + "YF.download() has changed argument auto_adjust default to True\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "import pandas as pd\n", + "import numpy as np\n", + "from typing import Optional, TypedDict, Literal, Dict, List, Any, Union, get_args, Iterable\n", + "from dataclasses import dataclass\n", + "from trade.datamanager.utils.model import LoadRequest, _load_model_data_timeseries, DivType, VolatilityModel, OptionPricingModel\n", + "from trade.datamanager.base import BaseDataManager\n", + "from trade.datamanager.config import OptionDataConfig\n", + "from trade.datamanager.result import _OptionModelResultsBase, Result\n", + "from trade.datamanager.result import (\n", + " VolatilityResult,\n", + " ForwardResult,\n", + " RatesResult,\n", + " OptionSpotResult,\n", + " SpotResult,\n", + " DividendsResult,\n", + ")\n", + "from trade.datamanager._enums import SeriesId, GreekType, OptionSpotEndpointSource, Interval, ArtifactType, RealTimeFallbackOption\n", + "from trade.datamanager.vars import LOADED_NAMES\n", + "from trade.datamanager.utils.model import ModelResultPack\n", + "from enum import Enum\n", + "from trade.datamanager.utils.cache import _check_cache_for_timeseries_data_structure\n", + "from trade.datamanager.utils.date import DATE_HINT\n", + "from trade.optionlib.greeks.numerical.binomial import binomial_tree_greeks\n", + "from trade.optionlib.greeks.numerical.black_scholes import vectorized_black_scholes_greeks\n", + "from trade.optionlib.assets.dividend import vectorized_discrete_pv, get_vectorized_continuous_dividends\n", + "from trade.datamanager.utils.date import sync_date_index, is_available_on_date, to_datetime\n", + "from trade.helpers.helper import change_to_last_busday\n", + "from trade.datamanager import DividendDataManager\n", + "from datetime import datetime\n", + "from trade.helpers.Logging import setup_logger\n", + "from trade.datamanager.utils.vol_helpers import _prepare_vol_calculation_setup, _handle_cache_for_vol, _merge_and_cache_vol_result\n", + "from trade.optionlib.assets.dividend import get_vectorized_dividend_scehdule, get_div_histories\n", + "LOADED_NAMES\n", + "logger = setup_logger(__name__)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4895aaec", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "95ff275c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mLoadRequest\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstart_date\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpandas\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_libs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtslibs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimestamps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTimestamp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mend_date\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpandas\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_libs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtslibs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimestamps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTimestamp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexpiration\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpandas\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_libs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtslibs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimestamps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTimestamp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstrike\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfloat\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mright\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mseries_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mSeriesId\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdividend_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moptionlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconfig\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtypes\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDivType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mendpoint_source\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOptionSpotEndpointSource\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mvol_model\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mVolatilityModel\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmarket_model\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOptionPricingModel\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_spot\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_forward\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_dividend\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_rates\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_option_spot\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_vol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mundo_adjust\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m LoadRequest(symbol: str, start_date: Union[str, pandas._libs.tslibs.timestamps.Timestamp], end_date: Union[str, pandas._libs.tslibs.timestamps.Timestamp], expiration: Union[str, pandas._libs.tslibs.timestamps.Timestamp], strike: Optional[float] = None, right: Optional[str] = None, series_id: Optional[trade.datamanager._enums.SeriesId] = None, dividend_type: Optional[trade.optionlib.config.types.DivType] = None, endpoint_source: Optional[trade.datamanager._enums.OptionSpotEndpointSource] = None, vol_model: Optional[trade.datamanager._enums.VolatilityModel] = None, market_model: Optional[trade.datamanager._enums.OptionPricingModel] = None, load_spot: bool = False, load_forward: bool = False, load_dividend: bool = False, load_rates: bool = False, load_option_spot: bool = False, load_vol: bool = False, undo_adjust: bool = True)\n", + "\u001b[0;31mFile:\u001b[0m ~/cloned_repos/QuantTools/trade/datamanager/requests.py\n", + "\u001b[0;31mType:\u001b[0m type\n", + "\u001b[0;31mSubclasses:\u001b[0m " + ] + } + ], + "source": [ + "LoadRequest?" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e0c6e5a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'BA': amount\n", + " ex_dividend_date \n", + " 1962-02-05 0.00823\n", + " 1962-05-08 0.00823\n", + " 1962-08-13 0.00823\n", + " 1962-11-05 0.00823\n", + " 1963-02-06 0.00823\n", + " ... ...\n", + " 2019-02-07 2.05500\n", + " 2019-05-09 2.05500\n", + " 2019-08-08 2.05500\n", + " 2019-11-07 2.05500\n", + " 2020-02-13 2.05500\n", + " \n", + " [228 rows x 1 columns]}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_div_histories(tickers=[\"BA\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "dc13e97e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-28 19:52:35 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker BA\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = get_vectorized_dividend_scehdule(\n", + " tickers=[\"BA\"], start_dates=[\"2025-01-01\"], end_dates=[\"2026-01-23\"], method=DividendDataManager.CONFIG.default_forecast_method.value\n", + ")\n", + "res" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "78f54a55", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "()" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tuple(res[-1])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "712a2d12", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "LoadRequest(symbol='BA', start_date='2025-01-01', end_date=Timestamp('2026-01-27 19:52:35.389174'), expiration='2026-08-21', strike=270.0, right='C', series_id=None, dividend_type=, endpoint_source=, vol_model=, market_model=, load_spot=False, load_forward=False, load_dividend=False, load_rates=False, load_option_spot=False, load_vol=False, undo_adjust=True)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "symbol = \"BA\"\n", + "expiration = \"2026-08-21\"\n", + "right = \"C\"\n", + "strike = 270.0\n", + "ts_start = \"2025-01-01\"\n", + "ts_end = datetime.now() - pd.tseries.offsets.BDay(1)\n", + "\n", + "LoadRequest(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " endpoint_source=OptionSpotEndpointSource.EOD,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " vol_model=VolatilityModel.MARKET,\n", + " dividend_type=DivType.DISCRETE,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ac4bc711", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-28 19:52:35 [test] trade.datamanager.vars INFO: Loading timeseries for BA...\n", + "2026-01-28 19:52:39 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker BA\n" + ] + }, + { + "data": { + "text/plain": [ + "([],\n", + " 'symbol:BA|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE')" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dm = DividendDataManager(\"BA\")\n", + "del dm.cache[\n", + " \"symbol:BA|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\"\n", + "]\n", + "dm.get_discrete_dividend_schedule(\n", + " end_date=ts_end,\n", + " start_date=ts_start,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "81667355", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mInit signature:\u001b[0m\n", + "\u001b[0mLoadRequest\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0msymbol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstart_date\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpandas\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_libs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtslibs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimestamps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTimestamp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mend_date\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpandas\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_libs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtslibs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimestamps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTimestamp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexpiration\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpandas\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_libs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtslibs\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimestamps\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTimestamp\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstrike\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mfloat\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mright\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mseries_id\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mSeriesId\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mdividend_type\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moptionlib\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconfig\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtypes\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mDivType\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mendpoint_source\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOptionSpotEndpointSource\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mvol_model\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mVolatilityModel\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mmarket_model\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mOptional\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_enums\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOptionPricingModel\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_spot\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_forward\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_dividend\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_rates\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_option_spot\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mload_vol\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mundo_adjust\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m LoadRequest(symbol: str, start_date: Union[str, pandas._libs.tslibs.timestamps.Timestamp], end_date: Union[str, pandas._libs.tslibs.timestamps.Timestamp], expiration: Union[str, pandas._libs.tslibs.timestamps.Timestamp], strike: Optional[float] = None, right: Optional[str] = None, series_id: Optional[trade.datamanager._enums.SeriesId] = None, dividend_type: Optional[trade.optionlib.config.types.DivType] = None, endpoint_source: Optional[trade.datamanager._enums.OptionSpotEndpointSource] = None, vol_model: Optional[trade.datamanager._enums.VolatilityModel] = None, market_model: Optional[trade.datamanager._enums.OptionPricingModel] = None, load_spot: bool = False, load_forward: bool = False, load_dividend: bool = False, load_rates: bool = False, load_option_spot: bool = False, load_vol: bool = False, undo_adjust: bool = True)\n", + "\u001b[0;31mFile:\u001b[0m ~/cloned_repos/QuantTools/trade/datamanager/requests.py\n", + "\u001b[0;31mType:\u001b[0m type\n", + "\u001b[0;31mSubclasses:\u001b[0m " + ] + } + ], + "source": [ + "LoadRequest?" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "69259a57", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-28 19:52:39 [test] trade.datamanager.vars INFO: Timeseries for BA already loaded.\n", + "2026-01-28 19:52:39 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-01-28 19:52:39 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 19:52:29.207345 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-01-28 19:52:39 [test] EventDriven.riskmanager.market_data INFO: Current time is after 6 PM NY time. Skipping sanitization of today's data.\n", + "2026-01-28 19:52:40 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-01-28 19:52:40 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-27...\n", + "2026-01-28 19:52:40 [test] trade.datamanager.vars INFO: Timeseries for BA already loaded.\n", + "2026-01-28 19:52:40 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-27 19:52:35.389174 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-01-28 19:52:40 [test] EventDriven.riskmanager.market_data INFO: Current time is after 6 PM NY time. Skipping sanitization of today's data.\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-27 19:52:35.389174...\n", + "2026-01-28 19:52:40 [test] trade.datamanager.vars INFO: Timeseries for BA already loaded.\n", + "2026-01-28 19:52:40 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:BA|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-08-21|use_chain_spot:1\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-27...\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-27 19:52:35.389174 and option tick BA20260821C270\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260821T000000|right:C|strike:270\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-27 00:00:00...\n", + "2026-01-28 19:52:40 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:BA|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260821T000000|right:C|strike:270\n", + "2026-01-28 19:52:40 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-27 19:52:35.389174 and option tick BA20260821C270\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260821T000000|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-27 00:00:00...\n", + "2026-01-28 19:52:40 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:BA|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260821T000000|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n" + ] + } + ], + "source": [ + "request = LoadRequest(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " dividend_type=DivType.CONTINUOUS,\n", + " load_spot = True,\n", + " load_forward=True,\n", + " load_vol=True,\n", + " load_dividend=True,\n", + " load_rates=True,\n", + " load_option_spot=True,\n", + " vol_model=VolatilityModel.MARKET,\n", + " market_model=OptionPricingModel.BSM\n", + ")\n", + "packet = _load_model_data_timeseries(request)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "17b1ff99", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(datetime\n", + " 2025-08-18 232.410004\n", + " 2025-08-19 225.000000\n", + " 2025-08-20 225.619995\n", + " 2025-08-21 224.460007\n", + " 2025-08-22 230.119995\n", + " ... \n", + " 2026-01-21 250.070007\n", + " 2026-01-22 251.410004\n", + " 2026-01-23 252.149994\n", + " 2026-01-26 248.429993\n", + " 2026-01-27 244.559998\n", + " Name: close, Length: 112, dtype: float64,\n", + " datetime\n", + " 2025-08-18 0.04123\n", + " 2025-08-19 0.04110\n", + " 2025-08-20 0.04115\n", + " 2025-08-21 0.04128\n", + " 2025-08-22 0.04088\n", + " ... \n", + " 2026-01-21 0.03588\n", + " 2026-01-22 0.03588\n", + " 2026-01-23 0.03582\n", + " 2026-01-26 0.03575\n", + " 2026-01-27 0.03570\n", + " Name: annualized, Length: 112, dtype: float64,\n", + " 2025-08-18 0.008842\n", + " 2025-08-19 0.009133\n", + " 2025-08-20 0.009108\n", + " 2025-08-21 0.009155\n", + " 2025-08-22 0.008930\n", + " ... \n", + " 2026-01-21 0.008218\n", + " 2026-01-22 0.008174\n", + " 2026-01-23 0.008150\n", + " 2026-01-26 0.008272\n", + " 2026-01-27 0.008403\n", + " Length: 112, dtype: float64,\n", + " datetime\n", + " 2025-08-18 0.312695\n", + " 2025-08-19 0.301072\n", + " 2025-08-20 0.310946\n", + " 2025-08-21 0.305072\n", + " 2025-08-22 0.318194\n", + " ... \n", + " 2026-01-21 0.338691\n", + " 2026-01-22 0.316445\n", + " 2026-01-23 0.325193\n", + " 2026-01-26 0.342815\n", + " 2026-01-27 0.320444\n", + " Length: 112, dtype: float64,\n", + " datetime\n", + " 2025-08-18 240.125869\n", + " 2025-08-19 232.350850\n", + " 2025-08-20 232.988232\n", + " 2025-08-21 231.789239\n", + " 2025-08-22 237.571665\n", + " ... \n", + " 2026-01-21 254.121131\n", + " 2026-01-22 255.469933\n", + " 2026-01-23 256.197111\n", + " 2026-01-26 252.332520\n", + " 2026-01-27 248.357683\n", + " Length: 112, dtype: float64)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "packet\n", + "s = packet.spot.timeseries\n", + "r = packet.rates.timeseries\n", + "d = packet.dividend.timeseries\n", + "vol = packet.vol.timeseries\n", + "f = packet.forward.timeseries\n", + "s, r, d, vol, f = sync_date_index(s, r, d, vol, f)\n", + "s, r, d, vol, f" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "bf4a1044", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([0.99112481, 0.99085866, 0.99090839, 0.99088648, 0.99113388,\n", + " 0.99108123, 0.99140601, 0.99145839, 0.99150147, 0.99147189,\n", + " 0.99166256, 0.99150796, 0.99147242, 0.99145457, 0.99157646,\n", + " 0.99154852, 0.99149908, 0.99123475, 0.99109694, 0.99116672,\n", + " 0.99116282, 0.99117283, 0.99124068, 0.99126616, 0.99119914,\n", + " 0.99139709, 0.99137366, 0.99133665, 0.99166328, 0.99158058,\n", + " 0.99155789, 0.99155922, 0.99167111, 0.99165359, 0.99185958,\n", + " 0.99196115, 0.99211032, 0.99179715, 0.99161936, 0.99188409,\n", + " 0.99186263, 0.99187736, 0.99182316, 0.99188954, 0.99211138,\n", + " 0.99215301, 0.99215462, 0.99222262, 0.99237316, 0.99250455,\n", + " 0.99254061, 0.99222757, 0.99173323, 0.9917995 , 0.99202236,\n", + " 0.99178981, 0.99180028, 0.99178215, 0.99173136, 0.99182581,\n", + " 0.99187109, 0.99191167, 0.99190228, 0.9919285 , 0.99190227,\n", + " 0.99183901, 0.99169709, 0.99143693, 0.99148318, 0.99154921,\n", + " 0.99173296, 0.99196018, 0.99210741, 0.9920902 , 0.99284345,\n", + " 0.99277107, 0.99277485, 0.99280325, 0.99303684, 0.99286044,\n", + " 0.9928295 , 0.99292819, 0.9930821 , 0.9932013 , 0.99326801,\n", + " 0.99328276, 0.99337198, 0.9935774 , 0.9937363 , 0.99376239,\n", + " 0.99382537, 0.99382816, 0.99392837, 0.99398861, 0.99397629,\n", + " 0.99430629, 0.99438887, 0.99445486, 0.99443771, 0.99444428,\n", + " 0.99463708, 0.99482491, 0.99494787, 0.99493066, 0.99505799,\n", + " 0.99507941, 0.99519544, 0.99523835, 0.99528596, 0.99532199,\n", + " 0.99531977, 0.9952688 ])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "get_vectorized_continuous_dividends(\n", + " div_rates=d,\n", + " _valuation_dates=s.index.tolist(),\n", + " _end_dates=[expiration]*len(s)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "52683838", + "metadata": {}, + "source": [ + "pv_divs = vectorized_discrete_pv(\n", + " schedules=d,\n", + " _valuation_dates=s.index.tolist(),\n", + " _end_dates=[expiration] * len(s),\n", + " r = r,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "eab39b65", + "metadata": {}, + "source": [ + "res = binomial_tree_greeks(\n", + " K = [strike] * len(s),\n", + " expiration=[expiration] * len(s),\n", + " sigma = vol,\n", + " S = s,\n", + " r = r,\n", + " N = [100] * len(s),\n", + " dividend_type=[DivType.CONTINUOUS.value] * len(s),\n", + " div_amount=d,\n", + " option_type=[right] * len(s),\n", + " start_date=s.index.tolist(),\n", + " valuation_date=s.index.tolist(),\n", + " american=[True]*len(s)\n", + ")\n" + ] + }, + { + "cell_type": "markdown", + "id": "fdd781ba", + "metadata": {}, + "source": [ + "res = vectorized_black_scholes_greeks(\n", + " S = s,\n", + " K = [strike] * len(s),\n", + " F = f,\n", + " r = r,\n", + " sigma = vol,\n", + " valuation_dates=s.index.tolist(),\n", + " end_dates=[expiration] * len(s),\n", + " option_type=[right.lower()] * len(s),\n", + " div_type=[DivType.CONTINUOUS.value] * len(s),\n", + " div_amount=pv_divs\n", + ")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7de1701d", + "metadata": {}, + "outputs": [], + "source": [ + "GREEKS = Literal[\n", + " GreekType.DELTA.value,\n", + " GreekType.GAMMA.value,\n", + " GreekType.THETA.value,\n", + " GreekType.VEGA.value,\n", + " GreekType.RHO.value,\n", + " GreekType.VOLGA.value,\n", + "]\n", + "AVAILABLE_GREEKS = get_args(GREEKS)\n", + "\n", + "@dataclass\n", + "class GreekResultSet(_OptionModelResultsBase):\n", + " key: Optional[str] = None\n", + " timeseries: Optional[pd.DataFrame] = None\n", + "\n", + " def is_empty(self) -> bool:\n", + " return self.timeseries is None or self.timeseries.empty\n", + " \n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " print(\"Called additional repr fields in GreekResultSet\")\n", + " super_additional = super()._additional_repr_fields()\n", + " return {\n", + " **super_additional,\n", + " \"Available Greeks\": [g for g in AVAILABLE_GREEKS if self.timeseries is not None and g in self.timeseries.columns],\n", + " \"empty\": self.is_empty(),\n", + " }\n", + "\n", + " def __repr__(self):\n", + " print(\"Called GreekResultSet repr\")\n", + " return super().__repr__()\n", + "\n", + " @property\n", + " def delta(self) -> Optional[pd.Series]:\n", + " if self.timeseries is not None and GreekType.DELTA.value in self.timeseries.columns:\n", + " return self.timeseries[GreekType.DELTA.value]\n", + " return None\n", + " \n", + " @property\n", + " def gamma(self) -> Optional[pd.Series]:\n", + " if self.timeseries is not None and GreekType.GAMMA.value in self.timeseries.columns:\n", + " return self.timeseries[GreekType.GAMMA.value]\n", + " return None\n", + " \n", + " @property\n", + " def theta(self) -> Optional[pd.Series]:\n", + " if self.timeseries is not None and GreekType.THETA.value in self.timeseries.columns:\n", + " return self.timeseries[GreekType.THETA.value]\n", + " return None\n", + " \n", + " @property\n", + " def vega(self) -> Optional[pd.Series]:\n", + " if self.timeseries is not None and GreekType.VEGA.value in self.timeseries.columns:\n", + " return self.timeseries[GreekType.VEGA.value]\n", + " return None\n", + " \n", + " @property\n", + " def rho(self) -> Optional[pd.Series]:\n", + " if self.timeseries is not None and GreekType.RHO.value in self.timeseries.columns:\n", + " return self.timeseries[GreekType.RHO.value]\n", + " return None\n", + " \n", + " @property\n", + " def volga(self) -> Optional[pd.Series]:\n", + " if self.timeseries is not None and GreekType.VOLGA.value in self.timeseries.columns:\n", + " return self.timeseries[GreekType.VOLGA.value]\n", + " return None\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "2a4a9d2a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Called GreekResultSet repr\n", + "Calling OptionModelResultsBase repr\n", + "Calling OptionResultsBase repr\n", + "Called additional repr fields in GreekResultSet\n" + ] + }, + { + "data": { + "text/plain": [ + "GreekResultSet(symbol=None, strike=None, expiration=None, right=None, model_price=, Available Greeks=[], empty=True)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "GreekResultSet()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a9fccb99", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def _prepare_greeks_to_compute(\n", + " greeks_to_compute: Optional[Union[GreekType, Iterable[GreekType]]] = None,\n", + ") -> List[GreekType]:\n", + " \n", + " ## If None, set to all greeks\n", + " if greeks_to_compute is None:\n", + " greeks_to_compute = GreekType.GREEKS\n", + " \n", + " ## Expand GREEKS to all greek types\n", + " if greeks_to_compute == GreekType.GREEKS:\n", + " greeks_to_compute = list(set(GreekType) - {GreekType.GREEKS, GreekType.VANNA})\n", + " \n", + " ## Validate greek_to_compute is list/tuple/set of GreekType\n", + " if not isinstance(greeks_to_compute, (list, np.ndarray, set, tuple)):\n", + " greeks_to_compute = [greeks_to_compute]\n", + "\n", + " ## Validate all elements are GreekType\n", + " if not all(isinstance(greek, GreekType) for greek in greeks_to_compute):\n", + " raise ValueError(f\"greeks_to_compute must be a GreekType or list of GreekType. Found: {greeks_to_compute}\")\n", + " \n", + " ## Validate no duplicates\n", + " greeks_to_compute = list(set(greeks_to_compute))\n", + "\n", + " return list(greeks_to_compute)\n", + "\n", + "_prepare_greeks_to_compute(GreekType.GREEKS)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "87552477", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def _get_prefilled_greek_result_set(\n", + " key: str,\n", + " symbol: str,\n", + " strike: float,\n", + " expiration: DATE_HINT,\n", + " right: str,\n", + " endpoint_source: OptionSpotEndpointSource,\n", + " market_model: OptionPricingModel,\n", + " vol_model: VolatilityModel,\n", + " dividend_type: DivType,\n", + ") -> GreekResultSet:\n", + " \"\"\"Utility to create prefilled GreekResultSet with metadata.\"\"\"\n", + " result = GreekResultSet(\n", + " key=key,\n", + " symbol=symbol,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " endpoint_source=endpoint_source,\n", + " market_model=market_model,\n", + " vol_model=vol_model,\n", + " dividend_type=dividend_type,\n", + " )\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9c69d188", + "metadata": {}, + "outputs": [], + "source": [ + "class GreekDataManager(BaseDataManager):\n", + " \"\"\"Data manager for option greeks calculation.\"\"\"\n", + " CONFIG = OptionDataConfig()\n", + " CACHE_NAME = \"greek_datamanager_cache\"\n", + " DEFAULT_SERIES_ID = SeriesId.HIST\n", + "\n", + " def __init__(self, symbol: str):\n", + " super().__init__(symbol=symbol)\n", + "\n", + " def get_greeks_timeseries(\n", + " self,\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: Optional[DivType] = None,\n", + " *,\n", + " result: Optional[GreekResultSet] = None,\n", + " greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS,\n", + " S: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " market_model: Optional[OptionPricingModel] = None,\n", + " undo_adjust: bool = True,\n", + "\n", + " ) -> GreekResultSet:\n", + " \"\"\"Get option greeks timeseries using binomial tree model.\"\"\"\n", + " dividend_type = dividend_type or self.CONFIG.dividend_type\n", + " endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source\n", + " market_model = market_model or self.CONFIG.option_model\n", + " vol_model = VolatilityModel.MARKET\n", + "\n", + "\n", + " result = _get_prefilled_greek_result_set(\n", + " key=None,\n", + " symbol=self.symbol,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " endpoint_source=endpoint_source,\n", + " market_model=market_model,\n", + " vol_model=vol_model,\n", + " dividend_type=dividend_type,\n", + " )\n", + " if market_model == OptionPricingModel.BINOMIAL:\n", + " return self._get_binomial_greeks(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " result=result,\n", + " greeks_to_compute=greeks_to_compute,\n", + " S=S,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " endpoint_source=endpoint_source,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " elif market_model == OptionPricingModel.BSM or market_model == OptionPricingModel.EURO_EQIV:\n", + " return self._get_bsm_greeks(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " result=result,\n", + " greeks_to_compute=greeks_to_compute,\n", + " f=S,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " endpoint_source=endpoint_source,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " else:\n", + " raise ValueError(f\"Unsupported market model: {market_model}\")\n", + "\n", + " def _get_binomial_greeks(\n", + " self,\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: Optional[DivType] = None,\n", + " *,\n", + " result: Optional[GreekResultSet] = None,\n", + " greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS,\n", + " S: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " undo_adjust: bool = True,\n", + "\n", + " ) -> GreekResultSet:\n", + " \n", + " ## biomial tree greeks calculation function calculates all greeks at once. So I'll check cache\n", + " ## for a greek and if missing, compute all and store in cache.\n", + " ## endpoint_source & div_type will resolved at `get_timeseries` level; the frontend function.\n", + " endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source\n", + " result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = (\n", + " _prepare_vol_calculation_setup(\n", + " self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result\n", + " )\n", + " )\n", + "\n", + " greeks_to_compute = _prepare_greeks_to_compute(greeks_to_compute)\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " interval=Interval.EOD,\n", + " artifact_type=ArtifactType.GREEKS,\n", + " series_id=SeriesId.HIST,\n", + " option_pricing_model=OptionPricingModel.BINOMIAL,\n", + " volatility_model=VolatilityModel.MARKET,\n", + " dividend_type=dividend_type,\n", + " endpoint_source=endpoint_source,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " )\n", + " result.key = key\n", + "\n", + " cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol(\n", + " self, key, start_date, end_date, result, optional_name=\"greeks\"\n", + " )\n", + " if early_return:\n", + " result.timeseries = cached_data[greeks_to_compute]\n", + " return result\n", + " \n", + " request = self._create_load_request(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " endpoint_source=endpoint_source,\n", + " s=S,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " model_data = _load_model_data_timeseries(request)\n", + " S = model_data.spot.timeseries\n", + " r = model_data.rates.timeseries\n", + " d = model_data.dividend.timeseries\n", + " vol = model_data.vol.timeseries\n", + " S, r, d, vol = sync_date_index(S, r, d, vol)\n", + " \n", + " ## Now compute greeks\n", + " greeks_res_dict = binomial_tree_greeks(\n", + " K = [strike] * len(S),\n", + " expiration=[expiration] * len(S),\n", + " sigma = vol,\n", + " S = S,\n", + " r = r,\n", + " N = [100] * len(S),\n", + " dividend_type=[dividend_type.value] * len(S),\n", + " div_amount=d,\n", + " option_type=[right] * len(S),\n", + " start_date=S.index.tolist(),\n", + " valuation_date=S.index.tolist(),\n", + " american=[True]*len(S)\n", + " )\n", + "\n", + " ## Remove \"models\" key if exists\n", + " if \"models\" in greeks_res_dict:\n", + " del greeks_res_dict[\"models\"]\n", + " \n", + " greeks_df = pd.DataFrame(greeks_res_dict, index=S.index)\n", + " \n", + " ## Use utility: Merge and cache\n", + " greeks_df = _merge_and_cache_vol_result(\n", + " self, greeks_df, cached_data, is_partial, key, start_str, end_str\n", + " )\n", + " result.timeseries = greeks_df[greeks_to_compute]\n", + "\n", + " return result\n", + "\n", + "\n", + " def _get_bsm_greeks(\n", + " self,\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: Optional[DivType] = None,\n", + " *,\n", + " result: Optional[GreekResultSet] = None,\n", + " greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS,\n", + " f: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " undo_adjust: bool = True,\n", + "\n", + " ) -> GreekResultSet:\n", + " \n", + " ## biomial tree greeks calculation function calculates all greeks at once. So I'll check cache\n", + " ## for a greek and if missing, compute all and store in cache.\n", + " ## endpoint_source & div_type will resolved at `get_timeseries` level; the frontend function.\n", + " endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source\n", + " result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = (\n", + " _prepare_vol_calculation_setup(\n", + " self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result\n", + " )\n", + " )\n", + "\n", + " greeks_to_compute = _prepare_greeks_to_compute(greeks_to_compute)\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " interval=Interval.EOD,\n", + " artifact_type=ArtifactType.GREEKS,\n", + " series_id=SeriesId.HIST,\n", + " option_pricing_model=OptionPricingModel.BSM,\n", + " volatility_model=VolatilityModel.MARKET,\n", + " dividend_type=dividend_type,\n", + " endpoint_source=endpoint_source,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " )\n", + " result.key = key\n", + "\n", + " cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol(\n", + " self, key, start_date, end_date, result, optional_name=\"greeks\"\n", + " )\n", + " if early_return:\n", + " result.timeseries = cached_data[greeks_to_compute]\n", + " return result\n", + " \n", + " request = self._create_load_request(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " market_model=OptionPricingModel.BSM,\n", + " endpoint_source=endpoint_source,\n", + " f=f,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " model_data = _load_model_data_timeseries(request)\n", + " S = model_data.spot.timeseries\n", + " r = model_data.rates.timeseries\n", + " d = model_data.dividend.timeseries\n", + " vol = model_data.vol.timeseries\n", + " f = model_data.forward.timeseries\n", + " s, f, r, d, vol = sync_date_index(S, f, r, d, vol)\n", + " \n", + " ## Convert dividends to present value amounts\n", + " if dividend_type == DivType.DISCRETE:\n", + " pv_divs = vectorized_discrete_pv(\n", + " schedules=d,\n", + " _valuation_dates=f.index.tolist(),\n", + " _end_dates=[expiration] * len(f),\n", + " r = r,\n", + " )\n", + "\n", + " \n", + " ## Continuous dividends. Discount dividend rates to present value amounts\n", + " else:\n", + " pv_divs = get_vectorized_continuous_dividends(\n", + " div_rates=d.values,\n", + " _valuation_dates=f.index.tolist(),\n", + " _end_dates=[expiration]*len(f)\n", + " )\n", + "\n", + " ## Now compute greeks\n", + " greeks_res_dict = vectorized_black_scholes_greeks(\n", + " S = s,\n", + " K = [strike] * len(s),\n", + " F = f,\n", + " r = r,\n", + " sigma = vol,\n", + " valuation_dates=s.index.tolist(),\n", + " end_dates=[expiration] * len(s),\n", + " option_type=[right.lower()] * len(s),\n", + " dividend_type=dividend_type.value,\n", + " div_amount=pv_divs\n", + " )\n", + " ## Remove \"models\" key if exists\n", + " if \"models\" in greeks_res_dict:\n", + " del greeks_res_dict[\"models\"]\n", + " \n", + " greeks_df = pd.DataFrame(greeks_res_dict, index=S.index)\n", + " \n", + " ## Use utility: Merge and cache\n", + " greeks_df = _merge_and_cache_vol_result(\n", + " self, greeks_df, cached_data, is_partial, key, start_str, end_str\n", + " )\n", + " result.timeseries = greeks_df[greeks_to_compute]\n", + "\n", + " return result\n", + "\n", + "\n", + " def get_at_time_greeks(\n", + " self,\n", + " as_of: DATE_HINT,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: Optional[DivType] = None,\n", + " *,\n", + " result: Optional[GreekResultSet] = None,\n", + " greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS,\n", + " S: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " market_model: Optional[OptionPricingModel] = None,\n", + " undo_adjust: bool = True,\n", + " fallback_option: Optional[RealTimeFallbackOption] = None,\n", + " ) -> GreekResultSet:\n", + " \n", + " vol_model = VolatilityModel.MARKET\n", + " dividend_type = dividend_type or self.CONFIG.dividend_type\n", + " endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source\n", + " market_model = market_model or self.CONFIG.option_model\n", + " fallback_option = fallback_option or self.CONFIG.real_time_fallback_option\n", + " if not is_available_on_date(as_of):\n", + " logger.warning(\n", + " f\"Valuation date {as_of} is not a business day or holiday. Resolving using fallback options {fallback_option}.\"\n", + " )\n", + " if fallback_option == RealTimeFallbackOption.RAISE_ERROR:\n", + " raise ValueError(f\"Valuation date {as_of} is not a business day or holiday.\")\n", + " if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE:\n", + " as_of = change_to_last_busday(as_of, eod_time=False)\n", + " else:\n", + " result = GreekResultSet()\n", + " v = float('nan') if fallback_option == RealTimeFallbackOption.NAN else 0.0\n", + " value_dict = {g: [v] for g in _prepare_greeks_to_compute(greeks_to_compute)}\n", + " result.timeseries = pd.DataFrame(data=value_dict,\n", + " index=pd.DatetimeIndex([to_datetime(as_of)]))\n", + " result.key = None\n", + " result.vol_model = vol_model or self.CONFIG.volatility_model\n", + " result.market_model = market_model or self.CONFIG.option_model\n", + " result.expiration = to_datetime(expiration)\n", + " result.right = right\n", + " result.strike = strike\n", + " result.endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source\n", + " result.dividend_type = dividend_type or self.CONFIG.dividend_type\n", + " result.symbol = self.symbol\n", + " return result\n", + " \n", + " greeks_result_set = self.get_greeks_timeseries(\n", + " start_date=as_of,\n", + " end_date=as_of,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " result=result,\n", + " greeks_to_compute=greeks_to_compute,\n", + " S=S,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " endpoint_source=endpoint_source,\n", + " market_model=market_model,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " return greeks_result_set\n", + " \n", + " ...\n", + "\n", + " def rt(\n", + " self,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: Optional[DivType] = None,\n", + " *,\n", + " result: Optional[GreekResultSet] = None,\n", + " greeks_to_compute: Optional[Union[List[GreekType], GreekType]] = GreekType.GREEKS,\n", + " S: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " market_model: Optional[OptionPricingModel] = None,\n", + " undo_adjust: bool = True,\n", + " fallback_option: Optional[RealTimeFallbackOption] = None,\n", + " ) -> GreekResultSet:\n", + " \n", + " return self.get_at_time_greeks(\n", + " as_of=datetime.now(),\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " result=result,\n", + " greeks_to_compute=greeks_to_compute,\n", + " S=S,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " endpoint_source=endpoint_source,\n", + " market_model=market_model,\n", + " undo_adjust=undo_adjust,\n", + " fallback_option=fallback_option,\n", + " )\n", + "\n", + " def _create_load_request(\n", + " \n", + " ## Requied parameters to ensure correct data is loaded\n", + " self,\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: DivType,\n", + " market_model: OptionPricingModel,\n", + " endpoint_source: OptionSpotEndpointSource,\n", + " *,\n", + "\n", + " ## Optional pre-loaded data. If not provided, will be loaded.\n", + " s: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " f: Optional[ForwardResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " undo_adjust: bool = True,\n", + " ) -> LoadRequest:\n", + " \n", + " req = LoadRequest(\n", + " symbol=self.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " endpoint_source=endpoint_source,\n", + " vol_model=VolatilityModel.MARKET,\n", + " market_model=market_model,\n", + "\n", + " ## Load spot only if missing.\n", + " load_spot=(s is None),\n", + " \n", + " ## Load forward only if missing and using BSM model. Binomial uses spot price.\n", + " load_forward=(market_model == OptionPricingModel.BSM) and (f is None),\n", + " load_vol=(vol is None),\n", + " load_dividend=(d is None),\n", + " load_rates=(r is None),\n", + "\n", + " ## Not needed for greek calculation\n", + " load_option_spot=False,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " return req\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "8b7c28d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 20:08:41.288079 - 2026-01-28 20:08:41.288079 and option tick BA20260821C270\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Binomial|right:C|strike:270|volatility_model:market\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 20:08:41.288079...\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Binomial|right:C|strike:270|volatility_model:market\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 20:08:41.366918 - 2026-01-28 20:08:41.366918 and option tick BA20260821C280\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 20:08:41.366918...\n", + "2026-01-28 20:08:41 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market\n" + ] + } + ], + "source": [ + "dm = GreekDataManager(\"BA\")\n", + "\n", + "long = {\n", + " \"strike\": 270.0,\n", + " \"expiration\": \"2026-08-21\",\n", + " \"right\": \"C\",\n", + "}\n", + "\n", + "short = {\n", + " \"strike\": 280.0,\n", + " \"expiration\": \"2026-08-21\",\n", + " \"right\": \"C\",\n", + "}\n", + "\n", + "vlong = dm.rt(**long)\n", + "vshort = dm.rt(**short)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "1e502711", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
deltavegarhogammavolgatheta
datetime
2026-01-280.0529990.0602270.0589960.0004570.000217-0.004645
\n", + "
" + ], + "text/plain": [ + " delta vega rho gamma volga theta\n", + "datetime \n", + "2026-01-28 0.052999 0.060227 0.058996 0.000457 0.000217 -0.004645" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vlong.timeseries - vshort.timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "bc37c57f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from trade.datamanager.utils.logging import get_datamanager_loggers, change_logging_in_all_datamanager_loggers\n", + "change_logging_in_all_datamanager_loggers(level=\"DEBUG\")\n", + "dm_loggers = get_datamanager_loggers()\n", + "dm_loggers" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "7af1b22d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 and option tick BA20260821C270\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-23 00:00:00...\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 and option tick BA20260821C270\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-23 00:00:00...\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 and option tick BA20260821C270\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Binomial|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-23 00:00:00...\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Binomial|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 and option tick BA20260821C270\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-23 00:00:00...\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 and option tick BA20260821C270\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-08-18 00:00:00 to 2026-01-23 00:00:00...\n", + "2026-01-28 19:52:45 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:BA|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:2026-08-21|option_pricing_model:Black-Scholes|right:C|strike:270|volatility_model:market\n" + ] + } + ], + "source": [ + "\n", + "grk = dm.get_greeks_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-23\",\n", + " expiration=\"2026-08-21\",\n", + " strike=270.0,\n", + " right=\"C\",\n", + " market_model=OptionPricingModel.BSM,\n", + " dividend_type=DivType.DISCRETE,\n", + " greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.THETA]\n", + ")\n", + "\n", + "grk_bsm_cont = dm.get_greeks_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-23\",\n", + " expiration=\"2026-08-21\",\n", + " strike=270.0,\n", + " right=\"C\",\n", + " market_model=OptionPricingModel.BSM,\n", + " dividend_type=DivType.CONTINUOUS,\n", + " greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.THETA]\n", + ")\n", + "grk_binom_cont = dm.get_greeks_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-23\",\n", + " expiration=\"2026-08-21\",\n", + " strike=270.0,\n", + " right=\"C\",\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " dividend_type=DivType.CONTINUOUS,\n", + " greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.THETA]\n", + ")\n", + "\n", + "grk_bsm_discrete = dm.get_greeks_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-23\",\n", + " expiration=\"2026-08-21\",\n", + " strike=270.0,\n", + " right=\"C\",\n", + " market_model=OptionPricingModel.BSM,\n", + " dividend_type=DivType.DISCRETE,\n", + " greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.THETA]\n", + ")\n", + "\n", + "greek_euro_eqiv = dm.get_greeks_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-23\",\n", + " expiration=\"2026-08-21\",\n", + " strike=270.0,\n", + " right=\"C\",\n", + " market_model=OptionPricingModel.EURO_EQIV,\n", + " dividend_type=DivType.DISCRETE,\n", + " greeks_to_compute=[GreekType.DELTA, GreekType.GAMMA, GreekType.THETA]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "ef6f0186", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "grk.delta.plot()\n", + "grk_bsm_cont.delta.plot()\n", + "grk_binom_cont.delta.plot()\n", + "grk_bsm_discrete.delta.plot()\n", + "greek_euro_eqiv.delta.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "87d63c56", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object `dm._load_model_data_timeseries` not found.\n" + ] + } + ], + "source": [ + "dm._load_model_data_timeseries?" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "bdc2a328", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openhighlowclosevolumebid_sizeclosebidask_sizecloseaskmidpointweighted_midpoint
datetime
2025-08-180.000.000.000.0001617.7014218.8518.27518.733544
2025-08-190.000.000.000.0008212.752115.9014.32513.392233
2025-08-200.000.000.000.0001514.90715.8015.35015.186364
2025-08-210.000.000.000.00011513.00815.8014.40013.182114
2025-08-220.000.000.000.0003316.759818.5017.62518.059160
....................................
2026-01-2118.5018.6318.5018.52832318.1051920.5019.30019.579335
2026-01-2218.8019.6918.3019.20571616.1044820.2518.17517.697251
2026-01-2319.0520.3019.0519.55954718.3010719.9019.10018.561774
2026-01-2618.0618.6517.9518.65812716.7040120.3518.52519.472064
2026-01-2715.2920.1514.0014.752469214.254716.1015.17514.367659
\n", + "

112 rows × 11 columns

\n", + "
" + ], + "text/plain": [ + " open high low close volume bid_size closebid ask_size \\\n", + "datetime \n", + "2025-08-18 0.00 0.00 0.00 0.00 0 16 17.70 142 \n", + "2025-08-19 0.00 0.00 0.00 0.00 0 82 12.75 21 \n", + "2025-08-20 0.00 0.00 0.00 0.00 0 15 14.90 7 \n", + "2025-08-21 0.00 0.00 0.00 0.00 0 115 13.00 8 \n", + "2025-08-22 0.00 0.00 0.00 0.00 0 33 16.75 98 \n", + "... ... ... ... ... ... ... ... ... \n", + "2026-01-21 18.50 18.63 18.50 18.52 8 323 18.10 519 \n", + "2026-01-22 18.80 19.69 18.30 19.20 5 716 16.10 448 \n", + "2026-01-23 19.05 20.30 19.05 19.55 9 547 18.30 107 \n", + "2026-01-26 18.06 18.65 17.95 18.65 8 127 16.70 401 \n", + "2026-01-27 15.29 20.15 14.00 14.75 24 692 14.25 47 \n", + "\n", + " closeask midpoint weighted_midpoint \n", + "datetime \n", + "2025-08-18 18.85 18.275 18.733544 \n", + "2025-08-19 15.90 14.325 13.392233 \n", + "2025-08-20 15.80 15.350 15.186364 \n", + "2025-08-21 15.80 14.400 13.182114 \n", + "2025-08-22 18.50 17.625 18.059160 \n", + "... ... ... ... \n", + "2026-01-21 20.50 19.300 19.579335 \n", + "2026-01-22 20.25 18.175 17.697251 \n", + "2026-01-23 19.90 19.100 18.561774 \n", + "2026-01-26 20.35 18.525 19.472064 \n", + "2026-01-27 16.10 15.175 14.367659 \n", + "\n", + "[112 rows x 11 columns]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "packet.option_spot.timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "a07cc60a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "isinstance(GreekType.GREEKS, Iterable)\n", + "type(GreekType.GREEKS)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/notebooks/option_data.ipynb b/trade/datamanager/notebooks/option_data.ipynb new file mode 100644 index 0000000..f73aae3 --- /dev/null +++ b/trade/datamanager/notebooks/option_data.ipynb @@ -0,0 +1,3356 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 39, + "id": "7aed1ab9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from trade.datamanager import (\n", + " BaseDataManager,\n", + " DividendDataManager,\n", + " OptionSpotDataManager,\n", + " ForwardDataManager,\n", + " SpotDataManager,\n", + " RatesDataManager,\n", + " Result,\n", + " SpotResult,\n", + " OptionSpotResult,\n", + " DividendsResult,\n", + " ForwardResult,\n", + " RatesResult,\n", + " CacheSpec\n", + ")\n", + "import importlib\n", + "import trade.datamanager as tdm\n", + "from trade.datamanager.result import _OptionResultsBase\n", + "from trade.datamanager._enums import (\n", + " SeriesId,\n", + " OptionSpotEndpointSource,\n", + " Interval,\n", + " ArtifactType,\n", + " OptionPricingModel,\n", + " VolatilityModel\n", + ")\n", + "from trade.datamanager.utils.cache import _data_structure_cache_it, _check_cache_for_timeseries_data_structure\n", + "from trade.datamanager.vars import TS\n", + "import pandas as pd\n", + "from typing import List, Dict, Any, Optional, Union, Tuple\n", + "import numpy as np\n", + "from dataclasses import dataclass\n", + "from trade.optionlib.vol.implied_vol import (\n", + " vector_vol_estimation,\n", + " bsm_vol_est_brute_force,\n", + " estimate_crr_implied_volatility,\n", + " crr_binomial_pricing,\n", + " bsm_vol_est_minimization,\n", + ")\n", + "from trade.optionlib.pricing.binomial import vector_crr_binomial_pricing\n", + "from trade.optionlib.utils.batch_operation import vector_batch_processor\n", + "from trade.datamanager.utils.date import time_distance_helper, _sync_date\n", + "from trade.optionlib.config.types import DivType\n", + "from trade.helpers.helper import to_datetime\n", + "from trade.optionlib.assets.dividend import vector_convert_to_time_frac\n", + "from trade.optionlib.utils.format import assert_equal_length, convert_to_array\n", + "from trade.datamanager.utils.date import DateRangePacket\n", + "from trade.helpers.Logging import setup_logger\n", + "\n", + "from typing import ClassVar\n", + "from trade.datamanager.config import OptionDataConfig\n", + "from trade.datamanager._enums import SeriesId\n", + "from datetime import datetime\n", + "from dbase.DataAPI.ThetaData import list_contracts\n", + "from trade.datamanager.utils.data_structure import _data_structure_sanitize\n", + "\n", + "logger = setup_logger(__name__, stream_log_level=\"DEBUG\")" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "0f53c643", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
rootexpirationstrikeright
1564AMD20271217210.0C
1565AMD20271217210.0P
1608AMD20281215210.0P
1610AMD20281215210.0C
1636AMD20271217220.0C
\n", + "
" + ], + "text/plain": [ + " root expiration strike right\n", + "1564 AMD 20271217 210.0 C\n", + "1565 AMD 20271217 210.0 P\n", + "1608 AMD 20281215 210.0 P\n", + "1610 AMD 20281215 210.0 C\n", + "1636 AMD 20271217 220.0 C" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "symbol = \"AMD\"\n", + "c = list_contracts(symbol, \"2026-01-16\")\n", + "c.query(\"strike > 200 and expiration > 20270701\").head(5)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d272e80d", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "expiration = \"2028-03-17\"\n", + "right = \"C\"\n", + "strike = 200.0\n", + "ts_start = \"2025-01-01\"\n", + "ts_end = \"2026-01-23\"" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fc4d7263", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class VolatilityResult(_OptionResultsBase):\n", + " \"\"\"Contains volatility surface data.\"\"\"\n", + " timeseries: Optional[pd.Series] = None\n", + " key: Optional[str] = None\n", + " model: Optional[VolatilityModel] = None\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None\n", + " market_model: Optional[OptionPricingModel] = None\n", + " div_type: Optional[DivType] = None\n", + " model_input_keys: Optional[Dict[str, Any]] = None\n", + "\n", + " def is_empty(self) -> bool:\n", + " \"\"\"Checks if volatility data is missing or empty.\"\"\"\n", + " return self.timeseries is None or self.timeseries.empty\n", + "\n", + " def _additional_repr_fields(self) -> Dict[str, Any]:\n", + " \"\"\"Provides volatility-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"symbol\": self.symbol,\n", + " \"expiration\": self.expiration,\n", + " \"right\": self.right,\n", + " \"strike\": self.strike,\n", + " \"model\": self.model,\n", + " \"endpoint_source\": self.endpoint_source,\n", + " \"market_model\": self.market_model,\n", + " \"div_type\": self.div_type,\n", + " \"key\": self.key,\n", + " \"is_empty\": self.is_empty(),\n", + " }\n", + "\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "b57ed2fe", + "metadata": {}, + "outputs": [], + "source": [ + "VolatilityResult().expiration" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "c342d2c9", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class ModelResultPack(Result):\n", + " \"\"\"\n", + " A container for various model result types.\n", + " \"\"\"\n", + " ## Main Results\n", + " spot: Optional[SpotResult] = None\n", + " forward: Optional[ForwardResult] = None\n", + " dividend: Optional[DividendsResult] = None\n", + " rates: Optional[RatesResult] = None\n", + " option_spot: Optional[OptionSpotResult] = None\n", + " vol: Optional[VolatilityResult] = None\n", + " \n", + " ## Guiding Enums\n", + " series_id: Optional[SeriesId] = None\n", + " dividend_type: Optional[DivType] = None\n", + " undo_adjust: bool = True\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None\n", + "\n", + " ## Diagnostic Info\n", + " time_to_load: Optional[Dict[str, float]] = None\n", + "\n", + " def _additional_repr_fields(self):\n", + " \"\"\"Provides model-specific fields for string representation.\"\"\"\n", + " return {\n", + " \"series_id\": self.series_id,\n", + " \"dividend_type\": self.dividend_type,\n", + " \"undo_adjust\": self.undo_adjust,\n", + " \"num_empty\": sum(\n", + " 1\n", + " for result in [\n", + " self.spot,\n", + " self.forward,\n", + " self.dividend,\n", + " self.rates,\n", + " self.option_spot,\n", + " self.vol,\n", + " ]\n", + " if result is None or result.is_empty()\n", + " ),\n", + " }\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "76fc305b", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class LoadRequest:\n", + " symbol: str\n", + " start_date: Union[str, pd.Timestamp]\n", + " end_date: Union[str, pd.Timestamp]\n", + " expiration: Union[str, pd.Timestamp]\n", + " strike: Optional[float] = None\n", + " right: Optional[str] = None\n", + " series_id: Optional[SeriesId] = None\n", + " div_type: Optional[DivType] = None\n", + " load_spot: bool = True\n", + " load_forward: bool = True\n", + " load_dividend: bool = True\n", + " load_rates: bool = True\n", + " load_option_spot: bool = False\n", + " undo_adjust: bool = True\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None\n", + "\n", + " def __post_init__(self):\n", + " if self.load_option_spot:\n", + " if self.strike is None or self.right is None:\n", + " raise ValueError(\"Strike and right must be provided when loading option spot data.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "b5cf92f7", + "metadata": {}, + "outputs": [], + "source": [ + "def assert_synchronized_model(\n", + " packet: ModelResultPack\n", + ") -> None:\n", + " \"\"\"\n", + " This function runs a comprehensive checks on every model inputs/data to ensure they are streamlined.\n", + "\n", + " Eg: \n", + " 1. If using discrete dividends, ensure all results that have dividend_type set to discrete. Same for continuous.\n", + " 2. If using undo_adjust, ensure all results have undo_adjust set to True.\n", + " 3. Ensure all results have the same date ranges where applicable. Or at least the same date range with option spot\n", + " \"\"\"\n", + "\n", + " ## Switch from using attribute names to searching for types\n", + " ## Check for all instances of Result subclasses in the packet\n", + "\n", + " ## 1. Dividend Type Consistency Check\n", + " div_types = set()\n", + " for result in [packet.spot, packet.forward, packet.dividend, packet.option_spot]:\n", + " div = getattr(result, \"dividend_type\", None)\n", + " if div is not None:\n", + " div_types.add(div)\n", + " if len(div_types) > 1:\n", + " raise ValueError(f\"Inconsistent dividend types across model results: {div_types}\")\n", + " \n", + " ## 2. Undo Adjust Consistency Check\n", + " undo_adjust_flags = set()\n", + " for result in [packet.spot, packet.forward, packet.dividend, packet.option_spot]:\n", + " ua = getattr(result, \"undo_adjust\", None)\n", + " if ua is not None:\n", + " undo_adjust_flags.add(ua)\n", + " if len(undo_adjust_flags) > 1:\n", + " raise ValueError(f\"Inconsistent undo_adjust flags across model results: {undo_adjust_flags}\")\n", + " \n", + " ## 3. Date Range Consistency Check\n", + " ## 4. Source Consistency Check\n", + " ## 5. Market Model Consistency Check\n", + " ## 6. Market Model Consistency Check" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "f2c0dbca", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "def _load_model_data_timeseries(\n", + " load_request: LoadRequest\n", + ") -> ModelResultPack:\n", + " \"\"\"\n", + " Loads model data based on the provided load request.\n", + "\n", + " Parameters:\n", + " load_request (LoadRequest): The request specifying what data to load.\n", + "\n", + " Returns:\n", + " ModelResultPack: A container with the loaded model data.\n", + " \"\"\"\n", + " load_info = {}\n", + " start_time = time.time()\n", + " packet = DateRangePacket(\n", + " start_date=load_request.start_date,\n", + " end_date=load_request.end_date,\n", + " maturity_date=load_request.expiration\n", + " )\n", + " load_info['date_range_packet'] = time.time() - start_time\n", + " symbol = load_request.symbol\n", + " start_date = packet.start_date\n", + " end_date = packet.end_date\n", + " expiration = packet.maturity_date\n", + " d = load_request.load_dividend\n", + " r = load_request.load_rates\n", + " s = load_request.load_spot\n", + " f = load_request.load_forward\n", + " opt_spot = load_request.load_option_spot\n", + " div_type = load_request.div_type or OptionDataConfig().dividend_type\n", + " D, R, S, F = None, None, None, None\n", + "\n", + " model_data = ModelResultPack()\n", + "\n", + " # Load BSM-specific data\n", + " if d:\n", + " start_time = time.time()\n", + " D = DividendDataManager(symbol).get_schedule_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " maturity_date=expiration,\n", + " div_type=div_type,\n", + " undo_adjust=load_request.undo_adjust\n", + " )\n", + " load_info['dividend_load_time'] = time.time() - start_time\n", + " if r:\n", + " start_time = time.time()\n", + " R = RatesDataManager().get_risk_free_rate_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date\n", + " )\n", + " load_info['rates_load_time'] = time.time() - start_time\n", + " if s:\n", + " start_time = time.time()\n", + " S = SpotDataManager(symbol=symbol).get_spot_timeseries(start_date=start_date,\n", + " end_date=end_date,\n", + " undo_adjust=load_request.undo_adjust)\n", + " load_info['spot_load_time'] = time.time() - start_time\n", + " if f:\n", + " start_time = time.time()\n", + " F = ForwardDataManager(symbol=symbol).get_forward_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " dividend_result=D,\n", + " maturity_date=expiration,\n", + " use_chain_spot=load_request.undo_adjust,\n", + " div_type=div_type\n", + " )\n", + " load_info['forward_load_time'] = time.time() - start_time\n", + " if opt_spot:\n", + " start_time = time.time()\n", + " market_price = OptionSpotDataManager(symbol=symbol).get_option_spot_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=load_request.strike,\n", + " right=load_request.right,\n", + " endpoint_source=load_request.endpoint_source\n", + " )\n", + " load_info['option_spot_load_time'] = time.time() - start_time\n", + " model_data.option_spot = market_price\n", + "\n", + "\n", + " \n", + " model_data.dividend = D\n", + " model_data.dividend_type = div_type\n", + " model_data.forward = F\n", + " model_data.rates = R\n", + " model_data.spot = S\n", + " model_data.series_id = SeriesId.HIST\n", + " model_data.undo_adjust = load_request.undo_adjust\n", + " model_data.time_to_load = load_info\n", + " model_data.endpoint_source = load_request.endpoint_source\n", + " assert_synchronized_model(model_data)\n", + "\n", + " return model_data\n", + "\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "0838be00", + "metadata": {}, + "outputs": [], + "source": [ + "def sync_date_index(*args) -> List[Union[pd.Series, pd.DataFrame]]:\n", + " \"\"\"Synchronizes the date indices of multiple time series.\"\"\"\n", + " for i, ts in enumerate(args):\n", + " if ts is None:\n", + " raise ValueError(\"All time series must be provided and not None. Found None at position {}\".format(i))\n", + " if not isinstance(ts, (pd.Series, pd.DataFrame)):\n", + " raise TypeError(\"All inputs must be pandas Series or DataFrame. Found {} at position {}\".format(type(ts), i))\n", + " date_indices = [set(ts.index) for ts in args if ts is not None]\n", + " common_dates = list(set.intersection(*date_indices))\n", + " synced_series = [ts.loc[common_dates] if ts is not None else None for ts in args]\n", + " synced_series = [ts.sort_index() if ts is not None else None for ts in synced_series]\n", + " return synced_series\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "38f6b860", + "metadata": {}, + "outputs": [], + "source": [ + "def vector_crr_iv_estimation(\n", + " S: List[float],\n", + " K: List[float],\n", + " T: List[float],\n", + " r: List[float],\n", + " market_price: List[float],\n", + " dividends: List[Any],\n", + " option_type: List[str],\n", + " N: List[int] = None,\n", + " dividend_type: List[str] = None,\n", + " american: List[bool] = None,\n", + ") -> List[float]:\n", + " \"\"\"Vectorized CRR implied volatility estimation.\"\"\"\n", + "\n", + " if not american:\n", + " american = [True] * len(S)\n", + "\n", + " if not dividend_type:\n", + " dividend_type = [\"discrete\"] * len(S)\n", + "\n", + " if not N:\n", + " N = [100] * len(S)\n", + "\n", + " assert_equal_length(\n", + " S,\n", + " K,\n", + " T,\n", + " r,\n", + " market_price,\n", + " dividends,\n", + " option_type,\n", + " N,\n", + " dividend_type,\n", + " american,\n", + " names=[\n", + " \"S\",\n", + " \"K\",\n", + " \"T\",\n", + " \"r\",\n", + " \"market_price\",\n", + " \"dividends\",\n", + " \"option_type\",\n", + " \"N\",\n", + " \"dividend_type\",\n", + " \"american\",\n", + " ],\n", + " )\n", + " if len(S) < 200:\n", + " print(\"Using non-batch processor for CRR implied volatility estimation.\")\n", + " return vector_vol_estimation(\n", + " estimate_crr_implied_volatility,\n", + " S,\n", + " K,\n", + " T,\n", + " r,\n", + " market_price,\n", + " dividends,\n", + " option_type,\n", + " N,\n", + " dividend_type,\n", + " american\n", + " )\n", + "\n", + " return vector_batch_processor(\n", + " vector_vol_estimation,\n", + " estimate_crr_implied_volatility,\n", + " S,\n", + " K,\n", + " T,\n", + " r,\n", + " market_price,\n", + " dividends,\n", + " option_type,\n", + " N,\n", + " dividend_type,\n", + " american\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "5149ddd3", + "metadata": {}, + "outputs": [], + "source": [ + "def vector_bsm_iv_estimation(\n", + " F: List[float],\n", + " K: List[float],\n", + " T: List[float],\n", + " r: List[float],\n", + " market_price: List[float],\n", + " right: List[str],\n", + ") -> List[float]:\n", + " \"\"\"Vectorized BSM implied volatility estimation.\"\"\"\n", + "\n", + " assert_equal_length(\n", + " F,\n", + " K,\n", + " T,\n", + " r,\n", + " market_price,\n", + " right,\n", + " names=[\n", + " \"F\",\n", + " \"K\",\n", + " \"T\",\n", + " \"r\",\n", + " \"market_price\",\n", + " \"right\",\n", + " ],\n", + " )\n", + " return vector_vol_estimation(bsm_vol_est_brute_force, F, K, T, r, market_price, right)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "bafd160f", + "metadata": {}, + "outputs": [], + "source": [ + "## Utility Functions for VolDataManager\n", + "\n", + "def _prepare_vol_calculation_setup(\n", + " manager: BaseDataManager,\n", + " start_date: str,\n", + " end_date: str,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " div_type: Optional[DivType],\n", + " endpoint_source: Optional[OptionSpotEndpointSource],\n", + " result: Optional[VolatilityResult] = None,\n", + ") -> Tuple[VolatilityResult, DivType, OptionSpotEndpointSource, str, str, datetime, datetime]:\n", + " \"\"\"Prepare common setup for volatility calculations.\"\"\"\n", + " result = VolatilityResult() if result is None else result\n", + " div_type = div_type or manager.CONFIG.dividend_type\n", + " endpoint_source = endpoint_source or manager.CONFIG.option_spot_endpoint_source\n", + " \n", + " start_date, end_date = _sync_date(\n", + " symbol=manager.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " endpoint_source=endpoint_source,\n", + " )\n", + " \n", + " start_str = to_datetime(start_date).strftime(\"%Y-%m-%d\")\n", + " end_str = to_datetime(end_date).strftime(\"%Y-%m-%d\")\n", + " \n", + " return result, div_type, endpoint_source, start_str, end_str, start_date, end_date\n", + "\n", + "\n", + "def _handle_cache_for_vol(\n", + " manager: BaseDataManager,\n", + " key: str,\n", + " start_date: datetime,\n", + " end_date: datetime,\n", + " result: VolatilityResult,\n", + ") -> Tuple[Optional[pd.Series], bool, datetime, datetime, Optional[VolatilityResult]]:\n", + " \"\"\"Handle cache checking logic for volatility calculations.\n", + " \n", + " Returns:\n", + " Tuple of (cached_data, is_partial, adjusted_start, adjusted_end, result_or_none)\n", + " If result_or_none is not None, caller should return it immediately (full cache hit)\n", + " \"\"\"\n", + " cached_data, is_partial, start_date, end_date = _check_cache_for_timeseries_data_structure(\n", + " key=key, self=manager, start_dt=start_date, end_dt=end_date\n", + " )\n", + " \n", + " if cached_data is not None and not is_partial:\n", + " logger.info(f\"Cache hit for vol timeseries key: {key}\")\n", + " result.timeseries = cached_data\n", + " return cached_data, is_partial, start_date, end_date, result\n", + " elif is_partial:\n", + " logger.info(f\"Cache partially covers requested date range. Key: {key}. Fetching missing dates.\")\n", + " else:\n", + " logger.info(f\"No cache found for key: {key}. Fetching from source.\")\n", + " \n", + " return cached_data, is_partial, start_date, end_date, None\n", + "\n", + "\n", + "def _merge_and_cache_vol_result(\n", + " manager: BaseDataManager,\n", + " iv_timeseries: pd.Series,\n", + " cached_data: Optional[pd.Series],\n", + " is_partial: bool,\n", + " key: str,\n", + " start_str: str,\n", + " end_str: str,\n", + ") -> pd.Series:\n", + " \"\"\"Merge with cache if partial, cache result, and sanitize.\"\"\"\n", + " # Merge with cached data if partial\n", + " print(f\"Start Date Str: {start_str}, End Date Str: {end_str}\")\n", + " if cached_data is not None and is_partial:\n", + " merged = pd.concat([cached_data, iv_timeseries])\n", + " iv_timeseries = merged[~merged.index.duplicated(keep=\"last\")].sort_index()\n", + " \n", + " # Cache the fetched data\n", + " _data_structure_cache_it(manager, key, iv_timeseries)\n", + " \n", + " # Sanitize before returning\n", + " iv_timeseries = _data_structure_sanitize(\n", + " iv_timeseries,\n", + " start=start_str,\n", + " end=end_str,\n", + " )\n", + " \n", + " return iv_timeseries\n", + "\n", + "\n", + "def _merge_provided_with_loaded_data(\n", + " model_data: \"ModelResultPack\",\n", + " S: Optional[SpotResult] = None,\n", + " F: Optional[ForwardResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " dividends: Optional[DividendsResult] = None,\n", + " market_price: Optional[OptionSpotResult] = None,\n", + ") -> Tuple[Optional[SpotResult], Optional[ForwardResult], RatesResult, Optional[DividendsResult], Optional[OptionSpotResult]]:\n", + " \"\"\"Merge user-provided data with loaded data, prioritizing provided data.\"\"\"\n", + " S = S if S is not None else model_data.spot\n", + " F = F if F is not None else model_data.forward\n", + " r = r if r is not None else model_data.rates\n", + " dividends = dividends if dividends is not None else model_data.dividend\n", + " market_price = market_price if market_price is not None else model_data.option_spot\n", + " \n", + " # Update model_data for consistency\n", + " if S is not None:\n", + " model_data.spot = S\n", + " if F is not None:\n", + " model_data.forward = F\n", + " if r is not None:\n", + " model_data.rates = r\n", + " if dividends is not None:\n", + " model_data.dividend = dividends\n", + " if market_price is not None:\n", + " model_data.option_spot = market_price\n", + " \n", + " return S, F, r, dividends, market_price\n", + "\n", + "\n", + "def _prepare_dividend_data_for_pricing(\n", + " dividends: DividendsResult,\n", + " div_type: DivType,\n", + " expiration: str,\n", + " *data_to_sync: pd.Series,\n", + ") -> Tuple[Any, ...]:\n", + " \"\"\"Prepare dividend data and synchronize all series.\n", + " \n", + " Returns:\n", + " Tuple of synchronized series (including prepared dividends as last element)\n", + " \"\"\"\n", + " if div_type == DivType.DISCRETE:\n", + " dividends_ts = dividends.daily_discrete_dividends\n", + " synced = sync_date_index(*data_to_sync, dividends_ts)\n", + " \n", + " # Convert to time fractions\n", + " dividends_prepared = vector_convert_to_time_frac(\n", + " schedules=synced[-1],\n", + " valuation_dates=synced[0].index,\n", + " end_dates=[to_datetime(expiration)] * len(synced[0].index)\n", + " )\n", + " return (*synced[:-1], dividends_prepared)\n", + " \n", + " elif div_type == DivType.CONTINUOUS:\n", + " dividends_ts = dividends.daily_continuous_dividends\n", + " synced = sync_date_index(*data_to_sync, dividends_ts)\n", + " return synced\n", + "\n", + "\n", + "def _prepare_time_to_expiration(\n", + " date_index: pd.DatetimeIndex,\n", + " expiration: str,\n", + ") -> List[float]:\n", + " \"\"\"Calculate time to expiration for each date in the index.\"\"\"\n", + " return [time_distance_helper(x, expiration) for x in date_index]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "756d49d2", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "\n", + "class VolDataManager(BaseDataManager):\n", + " CACHE_NAME: ClassVar[str] = \"vol_data_manager_cache\"\n", + " DEFAULT_SERIES_ID: ClassVar[\"SeriesId\"] = SeriesId.HIST\n", + " CONFIG = OptionDataConfig()\n", + " INSTANCES = {}\n", + "\n", + " def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> \"VolDataManager\":\n", + " if symbol not in cls.INSTANCES:\n", + " instance = object.__new__(cls)\n", + " cls.INSTANCES[symbol] = instance\n", + " return cls.INSTANCES[symbol]\n", + "\n", + " def __init__(\n", + " self, symbol: str, *, cache_spec: Optional[CacheSpec] = None, enable_namespacing: bool = False\n", + " ) -> None:\n", + " self.symbol = symbol\n", + " \n", + " if getattr(self, \"_initialized\", False):\n", + " return\n", + " self._initialized = True\n", + " super().__init__(\n", + " cache_spec=cache_spec,\n", + " enable_namespacing=enable_namespacing,\n", + " )\n", + "\n", + " def _get_bsm_implied_volatility_timeseries(\n", + " self,\n", + " start_date: str,\n", + " end_date: str,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " div_type: Optional[DivType] = DivType.DISCRETE,\n", + " *,\n", + " result: Optional[VolatilityResult] = None,\n", + " F: Optional[ForwardResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " market_price: Optional[OptionSpotResult] = None,\n", + " undo_adjust: bool = True,\n", + " ) -> VolatilityResult:\n", + " \"\"\"Retrieves BSM implied volatility timeseries.\"\"\"\n", + " \n", + " # Use utility: Prepare setup\n", + " endpoint_source = result.endpoint_source if result is not None else self.CONFIG.option_spot_endpoint_source\n", + " result, div_type, endpoint_source, start_str, end_str, start_date, end_date = _prepare_vol_calculation_setup(\n", + " self, start_date, end_date, expiration, strike, right, div_type, endpoint_source, result\n", + " )\n", + " \n", + " # Make key for caching\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " interval=Interval.EOD,\n", + " artifact_type=ArtifactType.IV,\n", + " SeriesId=SeriesId.HIST,\n", + " option_pricing_model=OptionPricingModel.BSM,\n", + " volatility_model=VolatilityModel.MARKET,\n", + " div_type=div_type,\n", + " endpoint_source=endpoint_source,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " )\n", + "\n", + " # Use utility: Handle cache\n", + " cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol(\n", + " self, key, start_date, end_date, result\n", + " )\n", + " if early_return is not None:\n", + " return early_return\n", + "\n", + " # Load model data\n", + " load_request = LoadRequest(\n", + " symbol=self.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " div_type=div_type,\n", + " load_spot=False,\n", + " load_forward=F is None,\n", + " load_rates=r is None,\n", + " load_option_spot=market_price is None,\n", + " strike=strike,\n", + " right=right,\n", + " undo_adjust=undo_adjust,\n", + " endpoint_source=endpoint_source\n", + " )\n", + " model_data = _load_model_data_timeseries(load_request)\n", + " \n", + " # Use utility: Merge provided data\n", + " _, F, r, _, market_price = _merge_provided_with_loaded_data(model_data, F=F, r=r, market_price=market_price)\n", + " \n", + " # Extract data\n", + " forward = F.daily_continuous_forward if div_type == DivType.CONTINUOUS else F.daily_discrete_forward\n", + " rates = r.daily_risk_free_rates\n", + " option_spot = market_price.daily_option_spot.midpoint\n", + " forward, rates, option_spot = sync_date_index(forward, rates, option_spot)\n", + " \n", + " # Use utility: Prepare T\n", + " T = _prepare_time_to_expiration(forward.index, expiration)\n", + " \n", + " # Calculate IV\n", + " iv_timeseries = vector_bsm_iv_estimation(\n", + " F=forward.values,\n", + " K=[strike] * len(forward),\n", + " T=T,\n", + " r=rates.values,\n", + " market_price=option_spot.values,\n", + " right=[right.lower()] * len(forward),\n", + " )\n", + " iv_timeseries = pd.Series(data=iv_timeseries, index=forward.index)\n", + "\n", + " # Use utility: Merge and cache\n", + " iv_timeseries = _merge_and_cache_vol_result(\n", + " self, iv_timeseries, cached_data, is_partial, key, start_str, end_str\n", + " )\n", + "\n", + " # Prepare result\n", + " vol = VolatilityResult(\n", + " timeseries=iv_timeseries,\n", + " key=key,\n", + " model=VolatilityModel.MARKET,\n", + " endpoint_source=endpoint_source,\n", + " market_model=OptionPricingModel.BSM,\n", + " div_type=div_type,\n", + " )\n", + " return vol\n", + "\n", + " def _get_crr_implied_volatility_timeseries(\n", + " self,\n", + " start_date: str,\n", + " end_date: str,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " div_type: Optional[DivType] = DivType.DISCRETE,\n", + " american: bool = True,\n", + " result: Optional[VolatilityResult] = None,\n", + " *,\n", + " S: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " dividends: Optional[DividendsResult] = None,\n", + " market_price: Optional[OptionSpotResult] = None,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + " ) -> VolatilityResult:\n", + " \"\"\"Retrieves CRR implied volatility timeseries.\"\"\"\n", + " \n", + " # Use utility: Prepare setup\n", + " endpoint_source = market_price.endpoint_source if market_price is not None else None\n", + " result, div_type, endpoint_source, start_str, end_str, start_date, end_date = _prepare_vol_calculation_setup(\n", + " self, start_date, end_date, expiration, strike, right, div_type, endpoint_source, result\n", + " )\n", + " n_steps = n_steps or self.CONFIG.n_steps\n", + "\n", + " # Make key for caching\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " interval=Interval.EOD,\n", + " artifact_type=ArtifactType.IV,\n", + " SeriesId=SeriesId.HIST,\n", + " option_pricing_model=OptionPricingModel.BINOMIAL,\n", + " volatility_model=VolatilityModel.MARKET,\n", + " div_type=div_type,\n", + " endpoint_source=endpoint_source,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " american=american,\n", + " n_steps=n_steps,\n", + " )\n", + " result.key = key\n", + "\n", + " # Use utility: Handle cache\n", + " cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol(\n", + " self, key, start_date, end_date, result\n", + " )\n", + " if early_return is not None:\n", + " return early_return\n", + "\n", + " # Load model data\n", + " load_request = LoadRequest(\n", + " symbol=self.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " div_type=div_type,\n", + " load_spot=S is None,\n", + " load_rates=r is None,\n", + " load_dividend=dividends is None,\n", + " load_option_spot=market_price is None,\n", + " strike=strike,\n", + " right=right,\n", + " undo_adjust=undo_adjust,\n", + " endpoint_source=endpoint_source,\n", + " )\n", + " model_data = _load_model_data_timeseries(load_request)\n", + "\n", + " # Use utility: Merge provided data\n", + " S, _, r, dividends, market_price = _merge_provided_with_loaded_data(\n", + " model_data, S=S, r=r, dividends=dividends, market_price=market_price\n", + " )\n", + " \n", + " # Extract data\n", + " spot = S.daily_spot\n", + " rates = r.daily_risk_free_rates\n", + " option_spot = market_price.midpoint\n", + " \n", + " # Use utility: Prepare dividends and sync\n", + " spot, rates, option_spot, dividends_ts = _prepare_dividend_data_for_pricing(\n", + " dividends, div_type, expiration, spot, rates, option_spot\n", + " )\n", + " \n", + " # Use utility: Prepare T\n", + " T = _prepare_time_to_expiration(option_spot.index, expiration)\n", + "\n", + " # Calculate IV\n", + " iv_timeseries = vector_crr_iv_estimation(\n", + " S=spot.values,\n", + " K=[strike] * len(spot),\n", + " T=T,\n", + " r=rates.values,\n", + " market_price=option_spot.values,\n", + " dividends=dividends_ts,\n", + " option_type=[right.lower()] * len(spot),\n", + " dividend_type=[div_type.name.lower()] * len(spot),\n", + " american=[american] * len(spot),\n", + " N=[n_steps] * len(spot)\n", + " )\n", + " iv_timeseries = pd.Series(data=iv_timeseries, index=spot.index)\n", + "\n", + " # Use utility: Merge and cache\n", + " iv_timeseries = _merge_and_cache_vol_result(\n", + " self, iv_timeseries, cached_data, is_partial, key, start_str, end_str\n", + " )\n", + "\n", + " # Prepare result\n", + " result.timeseries = iv_timeseries\n", + " return result\n", + " \n", + " def _get_european_equivalent_volatility_timeseries(\n", + " self,\n", + " start_date: str,\n", + " end_date: str,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " *,\n", + " result: Optional[VolatilityResult] = None,\n", + " crr_american_vols: VolatilityResult,\n", + " F: Optional[ForwardResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " dividends: Optional[DividendsResult] = None,\n", + " div_type: Optional[DivType] = DivType.DISCRETE,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + " ) -> VolatilityResult:\n", + " \"\"\"Convert CRR American implied volatilities to European equivalent volatilities.\"\"\"\n", + " \n", + " # Use utility: Prepare setup\n", + " endpoint_source = crr_american_vols.endpoint_source\n", + " result, div_type, endpoint_source, start_str, end_str, start_date, end_date = _prepare_vol_calculation_setup(\n", + " self, start_date, end_date, expiration, strike, right, div_type, endpoint_source, result\n", + " )\n", + "\n", + " # Make key for caching\n", + " key = self.make_key(\n", + " symbol=self.symbol,\n", + " interval=Interval.EOD,\n", + " artifact_type=ArtifactType.IV,\n", + " SeriesId=SeriesId.HIST,\n", + " option_pricing_model=OptionPricingModel.EURO_EQIV,\n", + " volatility_model=VolatilityModel.MARKET,\n", + " div_type=div_type,\n", + " endpoint_source=endpoint_source,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " )\n", + "\n", + " # Use utility: Handle cache\n", + " cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol(\n", + " self, key, start_date, end_date, result\n", + " )\n", + " if early_return is not None:\n", + " return early_return\n", + " \n", + " # Load model data\n", + " load_request = LoadRequest(\n", + " symbol=self.symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " div_type=div_type,\n", + " load_spot=True,\n", + " load_forward=F is None,\n", + " load_rates=r is None,\n", + " load_dividend=dividends is None,\n", + " strike=strike,\n", + " right=right,\n", + " undo_adjust=undo_adjust,\n", + " endpoint_source=endpoint_source,\n", + " )\n", + " model_data = _load_model_data_timeseries(load_request)\n", + "\n", + " # Use utility: Merge provided data\n", + " S, F, r, dividends, _ = _merge_provided_with_loaded_data(\n", + " model_data, S=model_data.spot, F=F, r=r, dividends=dividends\n", + " )\n", + " \n", + " # Extract data\n", + " spot = S.daily_spot\n", + " forward = F.daily_continuous_forward if div_type == DivType.CONTINUOUS else F.daily_discrete_forward\n", + " rates = r.daily_risk_free_rates\n", + " \n", + " # Prepare dividends based on type\n", + " if div_type == DivType.DISCRETE:\n", + " dividends_ts = dividends.daily_discrete_dividends\n", + " spot, forward, rates, dividends_ts, crr_american_iv = sync_date_index(\n", + " spot, forward, rates, dividends_ts, crr_american_vols.timeseries\n", + " )\n", + " dividends_ts = vector_convert_to_time_frac(\n", + " schedules=dividends_ts,\n", + " valuation_dates=spot.index,\n", + " end_dates=[to_datetime(expiration)] * len(spot.index)\n", + " )\n", + " dividend_yield = pd.Series(data=0.0, index=spot.index)\n", + " elif div_type == DivType.CONTINUOUS:\n", + " dividends_yield = dividends.daily_continuous_dividends\n", + " spot, forward, rates, dividend_yield, crr_american_iv = sync_date_index(\n", + " spot, forward, rates, dividends_yield, crr_american_vols.timeseries\n", + " )\n", + " dividends_ts = [()] * len(spot)\n", + "\n", + " # Price with CRR using American IVs in European mode\n", + " european_crr_price = vector_crr_binomial_pricing(\n", + " S0=spot.values,\n", + " K=[strike] * len(spot),\n", + " T=_prepare_time_to_expiration(spot.index, expiration),\n", + " r=rates.values,\n", + " sigma=crr_american_iv.values,\n", + " dividend_yield=dividend_yield.values,\n", + " dividends=dividends_ts,\n", + " right=[right.lower()] * len(spot),\n", + " N=[n_steps or self.CONFIG.n_steps] * len(spot),\n", + " dividend_type=[div_type.name.lower()] * len(spot),\n", + " american=[False] * len(spot),\n", + " )\n", + "\n", + " # Convert to BSM equivalent IV\n", + " european_equiv_iv = vector_bsm_iv_estimation(\n", + " F=forward.values,\n", + " K=[strike] * len(spot),\n", + " T=_prepare_time_to_expiration(spot.index, expiration),\n", + " r=rates.values,\n", + " market_price=european_crr_price,\n", + " right=[right.lower()] * len(spot),\n", + " )\n", + " european_equiv_iv = pd.Series(data=european_equiv_iv, index=spot.index)\n", + "\n", + " # Use utility: Merge and cache\n", + " european_equiv_iv = _merge_and_cache_vol_result(\n", + " self, european_equiv_iv, cached_data, is_partial, key, start_str, end_str\n", + " )\n", + " \n", + " # Prepare result\n", + " result.timeseries = european_equiv_iv\n", + " return result\n", + "\n", + " def get_implied_volatility_timeseries(\n", + " self,\n", + " start_date: str,\n", + " end_date: str,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " div_type: Optional[DivType] = DivType.DISCRETE,\n", + " american: bool = True,\n", + " *,\n", + " model: Optional[OptionPricingModel] = None,\n", + " S: Optional[SpotResult] = None,\n", + " F: Optional[ForwardResult] = None,\n", + " dividends: Optional[DividendsResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " market_price: Optional[OptionSpotResult] = None,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " ) -> VolatilityResult:\n", + " \"\"\"Retrieves implied volatility timeseries based on specified model.\"\"\"\n", + "\n", + " # Load model information\n", + " model = model or self.CONFIG.option_model\n", + " div_type = div_type or self.CONFIG.dividend_type\n", + " endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source\n", + " print(f\"Endpoint Source: {endpoint_source}\")\n", + "\n", + " # Prepare result container\n", + " result = VolatilityResult()\n", + " result.symbol = self.symbol\n", + " result.expiration = to_datetime(expiration)\n", + " result.right = right\n", + " result.strike = strike\n", + " result.div_type = div_type\n", + " result.model = self.CONFIG.volatility_model\n", + " result.endpoint_source = endpoint_source\n", + " result.market_model = model\n", + "\n", + " if model == OptionPricingModel.BSM:\n", + " return self._get_bsm_implied_volatility_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=div_type,\n", + " F=F,\n", + " r=r,\n", + " market_price=market_price,\n", + " undo_adjust=undo_adjust,\n", + " result=result,\n", + " )\n", + " elif model == OptionPricingModel.BINOMIAL:\n", + " return self._get_crr_implied_volatility_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=div_type,\n", + " S=S,\n", + " r=r,\n", + " dividends=dividends,\n", + " market_price=market_price,\n", + " undo_adjust=undo_adjust,\n", + " american=american,\n", + " n_steps=n_steps,\n", + " result=result,\n", + " )\n", + " elif model == OptionPricingModel.EURO_EQIV:\n", + " # First get the CRR American implied volatilities\n", + " crr_american_vols = self._get_crr_implied_volatility_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=div_type,\n", + " S=S,\n", + " r=r,\n", + " dividends=dividends,\n", + " market_price=market_price,\n", + " undo_adjust=undo_adjust,\n", + " american=True,\n", + " n_steps=n_steps,\n", + " )\n", + " return self._get_european_equivalent_volatility_timeseries(\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " crr_american_vols=crr_american_vols,\n", + " F=F,\n", + " r=r,\n", + " dividends=dividends,\n", + " div_type=div_type,\n", + " undo_adjust=undo_adjust,\n", + " n_steps=n_steps,\n", + " result=result,\n", + " )\n", + " else:\n", + " raise ValueError(f\"Unsupported option pricing model: {model}\")\n", + " \n", + " def get_at_time_implied_volatility(\n", + " self,\n", + " as_of: str,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " div_type: Optional[DivType] = DivType.DISCRETE,\n", + " american: bool = True,\n", + " *,\n", + " model: Optional[OptionPricingModel] = None,\n", + " S: Optional[SpotResult] = None,\n", + " F: Optional[ForwardResult] = None,\n", + " dividends: Optional[DividendsResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " market_price: Optional[OptionSpotResult] = None,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + " endpoint_source: Optional[OptionSpotEndpointSource] = None,\n", + " ) -> VolatilityResult:\n", + " \n", + " \"\"\"Retrieves implied volatility at a specific date based on specified model.\"\"\"\n", + " iv_timeseries = self.get_implied_volatility_timeseries(\n", + " start_date=as_of,\n", + " end_date=as_of,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=div_type,\n", + " american=american,\n", + " model=model,\n", + " S=S,\n", + " F=F,\n", + " dividends=dividends,\n", + " r=r,\n", + " market_price=market_price,\n", + " undo_adjust=undo_adjust,\n", + " n_steps=n_steps,\n", + " endpoint_source=endpoint_source,\n", + " )\n", + " iv_timeseries.timeseries = iv_timeseries.timeseries.loc[to_datetime(as_of) : to_datetime(as_of)]\n", + " return iv_timeseries\n", + "\n", + " def rt(\n", + " self,\n", + " expiration: str,\n", + " strike: float,\n", + " right: str,\n", + " div_type: Optional[DivType] = DivType.DISCRETE,\n", + " american: bool = True,\n", + " *,\n", + " model: Optional[OptionPricingModel] = None,\n", + " S: Optional[SpotResult] = None,\n", + " F: Optional[ForwardResult] = None,\n", + " dividends: Optional[DividendsResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " market_price: Optional[OptionSpotResult] = None,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + " ) -> VolatilityResult:\n", + " \"\"\"Returns a real-time VolatilityResult.\"\"\"\n", + " return self.get_at_time_implied_volatility(\n", + " as_of=datetime.now().strftime(\"%Y-%m-%d\"),\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=div_type,\n", + " american=american,\n", + " model=model,\n", + " S=S,\n", + " F=F,\n", + " dividends=dividends,\n", + " r=r,\n", + " market_price=market_price,\n", + " undo_adjust=undo_adjust,\n", + " n_steps=n_steps,\n", + " endpoint_source=OptionSpotEndpointSource.QUOTE\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "f72eb354", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fetching BSM discrete div IV timeseries...\n", + "Endpoint Source: OptionSpotEndpointSource.QUOTE\n", + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "2026-01-24 23:41:55 [test] __main__ INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|SeriesId:hist|div_type:discrete|endpoint_source:quote|expiration:2028-03-17|option_pricing_model:Black-Scholes|right:C|strike:200|volatility_model:market. Fetching from source.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-24 23:41:57 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker AAPL\n", + "2026-01-24 23:41:57 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 18, for original valuation: 10. Size from historical divs: 8\n", + "2026-01-24 23:41:57 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 10\n", + "2026-01-24 23:41:57 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26]\n", + "2026-01-24 23:41:57 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.24, 0.25, 0.25, 0.25, 0.25, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26]\n", + "2026-01-24 23:41:57 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2024, 2, 9), datetime.date(2024, 5, 10), datetime.date(2024, 8, 12), datetime.date(2024, 11, 8), datetime.date(2025, 2, 10), datetime.date(2025, 5, 12), datetime.date(2025, 8, 11), datetime.date(2025, 11, 10), datetime.date(2026, 2, 10), datetime.date(2026, 5, 10), datetime.date(2026, 8, 10), datetime.date(2026, 11, 10), datetime.date(2027, 2, 10), datetime.date(2027, 5, 10), datetime.date(2027, 8, 10), datetime.date(2027, 11, 10), datetime.date(2028, 2, 10), datetime.date(2028, 5, 10)]\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Using cached date range for 2025-12-04 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Start Date Str: 2025-12-04, End Date Str: 2026-01-23\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Fetched in 2.77 seconds.\n", + "\n", + "Fetching BSM continuous div IV timeseries...\n", + "Endpoint Source: OptionSpotEndpointSource.QUOTE\n", + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "2026-01-24 23:41:58 [test] __main__ INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|SeriesId:hist|div_type:continuous|endpoint_source:quote|expiration:2028-03-17|option_pricing_model:Black-Scholes|right:C|strike:200|volatility_model:market. Fetching from source.\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Using cached date range for 2025-12-04 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Start Date Str: 2025-12-04, End Date Str: 2026-01-23\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Fetched in 0.34 seconds.\n", + "\n", + "Fetching BINOMIAL discrete div IV timeseries...\n", + "Endpoint Source: OptionSpotEndpointSource.QUOTE\n", + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "2026-01-24 23:41:58 [test] __main__ INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|SeriesId:hist|american:0|div_type:discrete|endpoint_source:quote|expiration:2028-03-17|n_steps:100|option_pricing_model:Binomial|right:C|strike:200|volatility_model:market. Fetching from source.\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Using cached date range for 2025-12-04 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Using non-batch processor for CRR implied volatility estimation.\n", + "Start Date Str: 2025-12-04, End Date Str: 2026-01-23\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Fetched in 4.47 seconds.\n", + "\n", + "Fetching BINOMIAL continuous div IV timeseries...\n", + "Endpoint Source: OptionSpotEndpointSource.QUOTE\n", + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "2026-01-24 23:42:02 [test] __main__ INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|SeriesId:hist|american:0|div_type:continuous|endpoint_source:quote|expiration:2028-03-17|n_steps:100|option_pricing_model:Binomial|right:C|strike:200|volatility_model:market. Fetching from source.\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Using cached date range for 2025-12-04 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Using non-batch processor for CRR implied volatility estimation.\n", + "Start Date Str: 2025-12-04, End Date Str: 2026-01-23\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Fetched in 0.15 seconds.\n", + "\n" + ] + } + ], + "source": [ + "dm = VolDataManager(symbol=symbol)\n", + "dm.CONFIG.option_spot_endpoint_source = OptionSpotEndpointSource.QUOTE\n", + "dm.cache.clear()\n", + "\n", + "print(\"Fetching BSM discrete div IV timeseries...\")\n", + "starter = time.time()\n", + "bs_discrete = dm.get_implied_volatility_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=DivType.DISCRETE,\n", + " model=OptionPricingModel.BSM,\n", + ")\n", + "print(f\"Fetched in {time.time() - starter:.2f} seconds.\\n\")\n", + "\n", + "print(\"Fetching BSM continuous div IV timeseries...\")\n", + "starter = time.time()\n", + "bs_cont = dm.get_implied_volatility_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike, \n", + " right=right,\n", + " div_type=DivType.CONTINUOUS,\n", + " model=OptionPricingModel.BSM,\n", + ")\n", + "print(f\"Fetched in {time.time() - starter:.2f} seconds.\\n\")\n", + "\n", + "print(\"Fetching BINOMIAL discrete div IV timeseries...\")\n", + "starter = time.time()\n", + "crr_discrete = dm.get_implied_volatility_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=DivType.DISCRETE,\n", + " model=OptionPricingModel.BINOMIAL,\n", + " american=False,\n", + " n_steps=100,\n", + ")\n", + "print(f\"Fetched in {time.time() - starter:.2f} seconds.\\n\")\n", + "\n", + "print(\"Fetching BINOMIAL continuous div IV timeseries...\")\n", + "starter = time.time()\n", + "crr_cont = dm.get_implied_volatility_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=DivType.CONTINUOUS,\n", + " model=OptionPricingModel.BINOMIAL,\n", + " american=False,\n", + " n_steps=100,\n", + ")\n", + "print(f\"Fetched in {time.time() - starter:.2f} seconds.\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "39bcbcd5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Object `index.is_monotonic_increasing` not found.\n" + ] + } + ], + "source": [ + "pd.Series().index.is_monotonic_increasing?" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "49c03969", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Endpoint Source: OptionSpotEndpointSource.QUOTE\n", + "Using cached date range for 2026-01-24 00:00:00 - 2026-01-24 00:00:00 AAPL20280317C200\n", + "2026-01-24 23:42:03 [test] __main__ INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|SeriesId:hist|american:1|div_type:discrete|endpoint_source:quote|expiration:2028-03-17|n_steps:100|option_pricing_model:Binomial|right:C|strike:200|volatility_model:market. Fetching from source.\n", + "Sanitizing data from 2026-01-24 to 2026-01-24...\n", + "Sanitizing data from 2026-01-24 00:00:00 to 2026-01-24 00:00:00...\n", + "Sanitizing data from 2026-01-24 to 2026-01-24...\n", + "Using cached date range for 2026-01-24 00:00:00 - 2026-01-24 00:00:00 AAPL20280317C200\n", + "Sanitizing data from 2026-01-24 00:00:00 to 2026-01-24 00:00:00...\n", + "Using non-batch processor for CRR implied volatility estimation.\n", + "Start Date Str: 2026-01-24, End Date Str: 2026-01-24\n", + "Sanitizing data from 2026-01-24 to 2026-01-24...\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = dm.rt(\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=DivType.DISCRETE,\n", + " model=OptionPricingModel.BINOMIAL\n", + ")\n", + "res.endpoint_source" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "2b595692", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Series([], dtype: object)" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res.timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "a7eb8087", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n" + ] + }, + { + "data": { + "text/plain": [ + "(Timestamp('2025-12-04 00:00:00'), datetime.datetime(2026, 1, 23, 0, 0))" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# crr_cont.timeseries\n", + "pd.Timestamp(\"2023-01-03\").to_pydatetime()\n", + "_sync_date(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " endpoint_source=OptionSpotEndpointSource.QUOTE\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "0ae9a9f1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2026-01-23'" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts_end\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "be63b584", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "2026-01-24 23:42:04 [test] __main__ INFO: No cache found for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|SeriesId:hist|div_type:discrete|endpoint_source:quote|expiration:2028-03-17|option_pricing_model:European Equivalent|right:C|strike:200|volatility_model:market. Fetching from source.\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n", + "Start Date Str: 2025-12-04, End Date Str: 2026-01-23\n", + "Sanitizing data from 2025-12-04 to 2026-01-23...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2025-12-04 0.321819\n", + "2025-12-05 0.329942\n", + "2025-12-08 0.328443\n", + "2025-12-09 0.328443\n", + "2025-12-10 0.330317\n", + "2025-12-11 0.329318\n", + "2025-12-12 0.328693\n", + "2025-12-15 0.332692\n", + "2025-12-16 0.328943\n", + "2025-12-17 0.329567\n", + "2025-12-18 0.330067\n", + "2025-12-19 0.321444\n", + "2025-12-22 0.323694\n", + "2025-12-23 0.321069\n", + "2025-12-24 0.322069\n", + "2025-12-26 0.323444\n", + "2025-12-29 0.322194\n", + "2025-12-30 0.319819\n", + "2025-12-31 0.323943\n", + "2026-01-02 0.323444\n", + "2026-01-05 0.321444\n", + "2026-01-06 0.319944\n", + "2026-01-07 0.320444\n", + "2026-01-08 0.316570\n", + "2026-01-09 0.320319\n", + "2026-01-12 0.319194\n", + "2026-01-13 0.318694\n", + "2026-01-14 0.320819\n", + "2026-01-15 0.323694\n", + "2026-01-16 0.318569\n", + "2026-01-20 0.316945\n", + "2026-01-21 0.318569\n", + "2026-01-22 0.316070\n", + "2026-01-23 0.317695\n", + "dtype: float64" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res = dm._get_european_equivalent_volatility_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " crr_american_vols=crr_discrete,\n", + " div_type=DivType.DISCRETE,\n", + ")\n", + "\n", + "res.timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "62f7d45b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "res.timeseries.plot()\n", + "crr_discrete.timeseries.plot()\n", + "bs_discrete.timeseries.plot()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "a97715d9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-24 23:42:05 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker AAPL\n", + "2026-01-24 23:42:05 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 21, for original valuation: 13. Size from historical divs: 12\n", + "2026-01-24 23:42:05 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 9\n", + "2026-01-24 23:42:05 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26]\n", + "2026-01-24 23:42:05 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.23, 0.24, 0.24, 0.24, 0.24, 0.25, 0.25, 0.25, 0.25, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26, 0.26]\n", + "2026-01-24 23:42:05 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2023, 2, 10), datetime.date(2023, 5, 12), datetime.date(2023, 8, 11), datetime.date(2023, 11, 10), datetime.date(2024, 2, 9), datetime.date(2024, 5, 10), datetime.date(2024, 8, 12), datetime.date(2024, 11, 8), datetime.date(2025, 2, 10), datetime.date(2025, 5, 12), datetime.date(2025, 8, 11), datetime.date(2025, 11, 10), datetime.date(2026, 2, 10), datetime.date(2026, 5, 10), datetime.date(2026, 8, 10), datetime.date(2026, 11, 10), datetime.date(2027, 2, 10), datetime.date(2027, 5, 10), datetime.date(2027, 8, 10), datetime.date(2027, 11, 10), datetime.date(2028, 2, 10)]\n", + "Sanitizing data from 2025-01-01 00:00:00 to 2026-01-23 00:00:00...\n", + "Sanitizing data from 2025-01-01 to 2026-01-23...\n", + "Sanitizing data from 2025-01-01 00:00:00 to 2026-01-23 00:00:00...\n", + "Sanitizing data from 2025-01-02 to 2025-12-03...\n", + "Sanitizing data from 2025-01-01 00:00:00 to 2026-01-23 00:00:00...\n", + "Using cached date range for 2025-01-01 00:00:00 - 2026-01-23 00:00:00 AAPL20280317C200\n", + "Sanitizing data from 2025-12-04 00:00:00 to 2026-01-23 00:00:00...\n" + ] + }, + { + "data": { + "text/plain": [ + "ModelResultPack(series_id=, dividend_type=, undo_adjust=True, num_empty=1)" + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# crr_vol = crr_discrete.timeseries\n", + "model_data = _load_model_data_timeseries(\n", + " LoadRequest(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " div_type=DivType.DISCRETE,\n", + " load_spot=True,\n", + " load_forward=True,\n", + " load_dividend=True,\n", + " load_rates=True,\n", + " load_option_spot=True,\n", + " undo_adjust=True,\n", + " endpoint_source=OptionSpotEndpointSource.QUOTE\n", + " )\n", + ")\n", + "model_data" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "f4edb024", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openhighlowclosevolumebid_sizeclosebidask_sizecloseaskmidpointweighted_midpoint
datetime
2025-12-040.0109.5000.0106.2000.0274.0104.5010.0107.90106.200104.619718
2025-12-050.0107.7500.0105.2500.0153.0103.00262.0107.50105.250105.840964
2025-12-080.0105.5000.0104.3000.0311.0102.6022.0106.00104.300102.824625
2025-12-090.0106.3000.0103.7000.0400.0101.50399.0105.90103.700103.697247
2025-12-100.0105.9750.0105.1000.0392.0102.70388.0107.50105.100105.087692
2025-12-110.0105.0000.0104.2500.0436.0102.00433.0106.50104.250104.242232
2025-12-120.0105.5500.0104.2500.0153.0102.00154.0106.50104.250104.257329
2025-12-150.0103.4750.0101.0250.017.099.85134.0102.20101.025101.935430
2025-12-160.0101.9250.0101.0750.015.0100.1074.0102.05101.075101.721348
2025-12-170.0102.1750.098.7500.08.096.5011.0101.0098.75099.105263
2025-12-180.0100.0500.099.0000.063.097.8529.0100.1599.00098.575000
2025-12-190.099.4000.099.4000.0394.097.5587.0101.2599.40098.219231
2025-12-220.099.3250.097.2500.012.096.4040.098.1097.25097.707692
2025-12-230.099.0750.098.2000.083.097.2010.099.2098.20097.415054
2025-12-240.0101.2750.099.5250.033.098.8021.0100.2599.52599.363889
2025-12-250.00.0000.00.0000.00.00.000.00.000.0000.000000
2025-12-260.0100.6750.099.2000.08.098.1027.0100.3099.20099.797143
2025-12-290.099.8000.099.2750.083.098.2517.0100.3099.27598.598500
2025-12-300.099.4000.098.4500.0101.097.5556.099.3598.45098.192038
2025-12-310.098.9000.097.8000.016.096.8010.098.8097.80097.569231
2026-01-010.00.0000.00.0000.00.00.000.00.000.0000.000000
2026-01-020.0102.3000.096.9250.034.095.9052.097.9596.92597.139535
2026-01-050.096.5750.093.4250.013.092.802.094.0593.42592.966667
2026-01-060.091.4000.089.1750.035.088.3031.090.0589.17589.121970
2026-01-070.089.8250.087.5000.015.087.0029.088.0087.50087.659091
2026-01-080.086.0000.086.0000.0293.084.0018.088.0086.00084.231511
2026-01-090.087.4500.086.6250.010.085.8515.087.4086.62586.780000
2026-01-120.088.2250.087.2000.031.086.8029.087.6087.20087.186667
2026-01-130.088.2000.087.8500.07.087.3512.088.3587.85087.981579
2026-01-140.088.3000.087.1250.017.086.5516.087.7087.12587.107576
2026-01-150.087.6000.085.9500.014.085.0541.086.8585.95086.391818
2026-01-160.085.8500.083.2000.010.082.7549.083.6583.20083.497458
2026-01-190.00.0000.00.0000.00.00.000.00.000.0000.000000
2026-01-200.081.8750.075.8250.053.075.1524.076.5075.82575.570779
2026-01-210.079.4000.076.7500.082.076.0554.077.4576.75076.605882
2026-01-220.079.3500.077.0250.025.076.4538.077.6077.02577.143651
2026-01-230.077.8250.076.9000.014.076.3535.077.4576.90077.135714
\n", + "
" + ], + "text/plain": [ + " open high low ... closeask midpoint weighted_midpoint\n", + "datetime ... \n", + "2025-12-04 0.0 109.500 0.0 ... 107.90 106.200 104.619718\n", + "2025-12-05 0.0 107.750 0.0 ... 107.50 105.250 105.840964\n", + "2025-12-08 0.0 105.500 0.0 ... 106.00 104.300 102.824625\n", + "2025-12-09 0.0 106.300 0.0 ... 105.90 103.700 103.697247\n", + "2025-12-10 0.0 105.975 0.0 ... 107.50 105.100 105.087692\n", + "2025-12-11 0.0 105.000 0.0 ... 106.50 104.250 104.242232\n", + "2025-12-12 0.0 105.550 0.0 ... 106.50 104.250 104.257329\n", + "2025-12-15 0.0 103.475 0.0 ... 102.20 101.025 101.935430\n", + "2025-12-16 0.0 101.925 0.0 ... 102.05 101.075 101.721348\n", + "2025-12-17 0.0 102.175 0.0 ... 101.00 98.750 99.105263\n", + "2025-12-18 0.0 100.050 0.0 ... 100.15 99.000 98.575000\n", + "2025-12-19 0.0 99.400 0.0 ... 101.25 99.400 98.219231\n", + "2025-12-22 0.0 99.325 0.0 ... 98.10 97.250 97.707692\n", + "2025-12-23 0.0 99.075 0.0 ... 99.20 98.200 97.415054\n", + "2025-12-24 0.0 101.275 0.0 ... 100.25 99.525 99.363889\n", + "2025-12-25 0.0 0.000 0.0 ... 0.00 0.000 0.000000\n", + "2025-12-26 0.0 100.675 0.0 ... 100.30 99.200 99.797143\n", + "2025-12-29 0.0 99.800 0.0 ... 100.30 99.275 98.598500\n", + "2025-12-30 0.0 99.400 0.0 ... 99.35 98.450 98.192038\n", + "2025-12-31 0.0 98.900 0.0 ... 98.80 97.800 97.569231\n", + "2026-01-01 0.0 0.000 0.0 ... 0.00 0.000 0.000000\n", + "2026-01-02 0.0 102.300 0.0 ... 97.95 96.925 97.139535\n", + "2026-01-05 0.0 96.575 0.0 ... 94.05 93.425 92.966667\n", + "2026-01-06 0.0 91.400 0.0 ... 90.05 89.175 89.121970\n", + "2026-01-07 0.0 89.825 0.0 ... 88.00 87.500 87.659091\n", + "2026-01-08 0.0 86.000 0.0 ... 88.00 86.000 84.231511\n", + "2026-01-09 0.0 87.450 0.0 ... 87.40 86.625 86.780000\n", + "2026-01-12 0.0 88.225 0.0 ... 87.60 87.200 87.186667\n", + "2026-01-13 0.0 88.200 0.0 ... 88.35 87.850 87.981579\n", + "2026-01-14 0.0 88.300 0.0 ... 87.70 87.125 87.107576\n", + "2026-01-15 0.0 87.600 0.0 ... 86.85 85.950 86.391818\n", + "2026-01-16 0.0 85.850 0.0 ... 83.65 83.200 83.497458\n", + "2026-01-19 0.0 0.000 0.0 ... 0.00 0.000 0.000000\n", + "2026-01-20 0.0 81.875 0.0 ... 76.50 75.825 75.570779\n", + "2026-01-21 0.0 79.400 0.0 ... 77.45 76.750 76.605882\n", + "2026-01-22 0.0 79.350 0.0 ... 77.60 77.025 77.143651\n", + "2026-01-23 0.0 77.825 0.0 ... 77.45 76.900 77.135714\n", + "\n", + "[37 rows x 11 columns]" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_data.option_spot.daily_option_spot" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "5094ef8f", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'crr_vol' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[34], line 6\u001b[0m\n\u001b[1;32m 1\u001b[0m S, D, R, OPTION_MID, SIGMA, F \u001b[38;5;241m=\u001b[39m sync_date_index(\n\u001b[1;32m 2\u001b[0m model_data\u001b[38;5;241m.\u001b[39mspot\u001b[38;5;241m.\u001b[39mdaily_spot,\n\u001b[1;32m 3\u001b[0m model_data\u001b[38;5;241m.\u001b[39mdividend\u001b[38;5;241m.\u001b[39mdaily_discrete_dividends,\n\u001b[1;32m 4\u001b[0m model_data\u001b[38;5;241m.\u001b[39mrates\u001b[38;5;241m.\u001b[39mdaily_risk_free_rates,\n\u001b[1;32m 5\u001b[0m model_data\u001b[38;5;241m.\u001b[39moption_spot\u001b[38;5;241m.\u001b[39mmidpoint,\n\u001b[0;32m----> 6\u001b[0m \u001b[43mcrr_vol\u001b[49m,\n\u001b[1;32m 7\u001b[0m model_data\u001b[38;5;241m.\u001b[39mforward\u001b[38;5;241m.\u001b[39mdaily_discrete_forward,\n\u001b[1;32m 8\u001b[0m )\n\u001b[1;32m 10\u001b[0m european_equity_prices \u001b[38;5;241m=\u001b[39m vector_crr_binomial_pricing(\n\u001b[1;32m 11\u001b[0m S0\u001b[38;5;241m=\u001b[39mS\u001b[38;5;241m.\u001b[39mvalues,\n\u001b[1;32m 12\u001b[0m K\u001b[38;5;241m=\u001b[39m[strike] \u001b[38;5;241m*\u001b[39m \u001b[38;5;28mlen\u001b[39m(S),\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 24\u001b[0m dividend_type\u001b[38;5;241m=\u001b[39m[DivType\u001b[38;5;241m.\u001b[39mDISCRETE\u001b[38;5;241m.\u001b[39mname\u001b[38;5;241m.\u001b[39mlower()] \u001b[38;5;241m*\u001b[39m \u001b[38;5;28mlen\u001b[39m(S)\n\u001b[1;32m 25\u001b[0m )\n\u001b[1;32m 27\u001b[0m eq_prices \u001b[38;5;241m=\u001b[39m pd\u001b[38;5;241m.\u001b[39mSeries(data\u001b[38;5;241m=\u001b[39meuropean_equity_prices, index\u001b[38;5;241m=\u001b[39mS\u001b[38;5;241m.\u001b[39mindex)\n", + "\u001b[0;31mNameError\u001b[0m: name 'crr_vol' is not defined" + ] + } + ], + "source": [ + "S, D, R, OPTION_MID, SIGMA, F = sync_date_index(\n", + " model_data.spot.daily_spot,\n", + " model_data.dividend.daily_discrete_dividends,\n", + " model_data.rates.daily_risk_free_rates,\n", + " model_data.option_spot.midpoint,\n", + " crr_vol,\n", + " model_data.forward.daily_discrete_forward,\n", + ")\n", + "\n", + "european_equity_prices = vector_crr_binomial_pricing(\n", + " S0=S.values,\n", + " K=[strike] * len(S),\n", + " T=[time_distance_helper(x, expiration) for x in S.index],\n", + " r=R.values,\n", + " dividends = vector_convert_to_time_frac(\n", + " schedules=D,\n", + " valuation_dates=S.index,\n", + " end_dates=[to_datetime(expiration)] * len(S.index)\n", + " ),\n", + " right=[right.lower()] * len(S),\n", + " american=[False] * len(S),\n", + " N=[1000] * len(S),\n", + " sigma=SIGMA.loc[S.index].values,\n", + " dividend_type=[DivType.DISCRETE.name.lower()] * len(S)\n", + ")\n", + "\n", + "eq_prices = pd.Series(data=european_equity_prices, index=S.index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "778f44bf", + "metadata": {}, + "outputs": [], + "source": [ + "bsm_eq_equiv_vols = vector_bsm_iv_estimation(\n", + " F=F.values,\n", + " K=[strike] * len(F),\n", + " T=[time_distance_helper(x, expiration) for x in F.index],\n", + " r=R.values,\n", + " market_price=eq_prices.values,\n", + " right=[right.lower()] * len(F),\n", + ")\n", + "bsm_vols = pd.Series(data=bsm_eq_equiv_vols, index=F.index)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "693f4b02", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 118, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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ioiJFRERERERURExsSIiIiIiIioi9gqYh/j4eKjVamOHQQBcXFwQExNj7DCoGLAuTQfrkkoSbo+mgfVoOkypLhUKBRwcHJ5f7iXEUmqp1WpkZWUZO4wyT5IkAE/qg51Ylm6sS9PBuqSShNujaWA9mo6yWpdsCkhERERERFRETKyIiIiIiIiKiIkVERERERFREfEZKyIiIqICUKvVSE1NNXYYJiktLQ2ZmZnGDoOKQWmrS0tLSygURUuNmFgRERER5ZNarUZKSgpsbGwgk7HhT3FTKpXsOMxElKa61Gg0ePz4MaysrIqUXPGIQERERJRPqampTKqITIxMJoONjU2R70TzqEBERERUAEyqiExPcezXPDIQEREREREVERMrIiIiIiKiImJiRUREREREVERMrIiIiIiISqH69etjyZIlpX4Zz7NmzRpUqVLFqDHkBxMrIiIiojIgOjoa06ZNQ8OGDeHr64u6deuif//+OHDggK5M/fr14enpCU9PT/j5+aFVq1b47bff9OZz+PBhXRlPT09Ur14db775JsLCwvJc/rPTPf0XHR39Qta5tBg7dqzuu/Dx8UFoaCjmz58PtVqd53R//fUX3njjjZcUZe7i4+Mxffp01KtXDz4+PqhTpw7GjBmDe/fuFXhehhK5V199VW87Lan4HisiIiIiE3f37l107doVtra2mDZtGgIDA6FWq/Hvv/9i6tSp2L9/v67shAkT0K9fP6SlpWHbtm14//334ebmhpYtW+rNc//+/bCxscHDhw/xySef4K233sKhQ4egUqnyjEU73dOcnZ0LvW6ZmZnPXWZp0KJFC8ybNw+ZmZnYvXs3pk6dCoVCgdGjR+coq11nJycnI0SqLz4+Hp07d4ZKpcIXX3yBypUr4+7du5gzZw46dOiALVu2wNvbu0jLsLCwgIWFRTFF/OLwjlUJ9zA5E5nZGmOHQURERAYIISAy0o3zJ0S+45wyZQoA4M8//0THjh3h5+eHypUrY+jQodi6dateWWtra7i6usLb2xsjR46Evb29XuKl5ezsDFdXV1SvXh2DBw/G/fv3cf369efGop3u6T9tV9ddu3bF9OnT9coPHDgQY8eO1X2uX78+5s+fj3fffReVK1fGxIkTdevWokUL+Pr6on79+li0aJHefLTTjRgxAv7+/ggODsaKFSv0yiQmJmLChAmoXr06Kleu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oCAASkpKCA4Odr6npKSE6OhocwoWERERcUFRvbvRZvcBTlp9STtyjB69os0uSaRFa3GzAjbw8fEhODiY8vJy9u7dy5AhQ5zhat++fc7lKioqSElJoWfPniZWKyIiIuJaPDy96F1XCMD+lGyTqxFp+VrcGas9e/YAEBERQW5uLgsXLqRTp04kJCRgGAYTJkxg+fLldOzYkbCwMD788EOCg4OdswSKiIiISL24tg521sLBwhquM7sYkRauxQWriooKFi1aRGFhIf7+/gwbNowZM2ZgtdavyuTJk6murubNN9+koqKC2NhYnnzySby8vEyuXERERMS1XNSlPaRAkiMQh8PR6qbHFmlKLS5YjRgxghEjRvzk64ZhMG3aNKZNm9aMVYmIiIi0PN369MA7+QhlVl+OHcsjKqqD2SWJtFgt9h4rEREREWkcrzZt6FmTB8CB5GMmVyPSsilYiYiIiLRicb42AJLyKkyuRKRlU7ASERERacXiIoMASKr1NbcQkRZOwUpERESkFYu9qDsedhsFnm05XlBidjkiLZaClYiIiEgr5hMaSveq4wAcSEo3txiRFkzBSkRERKSVi/OqAmBzlu6zEjlfClYiIiIirdyl3YPwsNvYaQ9mR3K22eWItEgKViIiIiKtXNSIi7mm/AAAC7bnUF1nN7kikZZHwUpERESklTMsFqZdMZB2VcUct7Rh6TdJZpck0uIoWImIiIgIft1iuN0nE4Dl2QaZJ06aXJFIy6JgJSIiIiIADJ98BQNKjlBn8eAfaw/icDjMLkmkxVCwEhEREREALP5tuTOuDZ72WvbW+vPvJE1kIXK2FKxERERExClizBiuL9sHwFu78qiotZlckUjLoGAlIiIiIk6GxcKUCcMIryygyOLLovUHzS5JpEVQsBIRERGRU3hHd+dOvxwAPs+zkFZQbnJFIq5PwUpERERETjPo2qsYfuIgdsPCG18fxq6JLER+loKViIiIiJzG8PPntr7B+NiqOVTnx7p9mWaXJOLSFKxERERE5Izajx7LDWWJALyzt4jSak1kIfJTFKxERERE5IwMw2DSNSPpfDKXMos3CzWRhchPUrASERERkZ/kGdWNewLyAFhb4EFybpnJFYm4JgUrEREREflZF026mksKE3EYBm9sSMFm10QWIj+mYCUiIiIiP8vwa8PMgeG0qa0g1ebLqj3HzC5J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ERGRRlKwEhERERERaSQFKxERERERkUZSsBIREREREWkkBSsREREREZFGUrASERERERFpJKvZBYiIiDS1DRs28Nprrzl/9vT0xN/fn6ioKAYMGMAll1yCr6/vOX9ucnIye/fu5eqrr6ZNmzZNWbKIiLRwClYiIuK2brjhBsLCwrDZbBQXF5OUlMS7777LypUreeyxx+jSpcs5fV5ycjJLly4lISFBwUpERE6hYCUiIm5rwIABdO/e3fnzddddx/79+3n++ed58cUXmT9/Pl5eXiZWKCIi7kLBSkREWpU+ffowZcoUFi1axMaNGxk/fjxHjx7l888/5+DBg5w4cQI/Pz8GDBjAzTffTNu2bQFYsmQJS5cuBWD27NnOz/v73/9OWFgYABs3bmTlypVkZmbi5eVFv379uOmmmwgNDW3+FRURkWalYCUiIq3OmDFjWLRoEYmJiYwfP57ExETy8vJISEggKCiIzMxMvvrqKzIzM5k7dy6GYTBs2DBycnLYvHkzt9xyizNwBQQEALB8+XIWL17M8OHDGTduHKWlpaxatYqnnnqKF198UZcOioi4OQUrERFpddq1a4efnx/Hjx8H4IorruCaa645ZZkePXrw0ksvcejQIXr37k2XLl3o2rUrmzdvZsiQIc6zVAD5+fksWbKEadOmcf311zt/P3ToUB5//HFWr159yu9FRMT9aLp1ERFplXx8fKisrAQ45T6rmpoaSktL6dGjBwBpaWm/+Fnbtm3D4XAwYsQISktLnf8EBQURHh7OgQMHLsxKiIiIy9AZKxERaZWqqqoIDAwEoLy8nI8++ogtW7ZQUlJyynIVFRW/+Fm5ubk4HA4eeOCBM75utWpzKyLi7vSXXkREWp3CwkIqKiro0KEDAPPnzyc5OZlJkyYRHR2Nj48PdrudefPmYbfbf/Hz7HY7hmHwxBNPYLGcfjGIj49Pk6+DiIi4FgUrERFpdTZu3AhA//79KS8vZ9++fdxwww1MnTrVuUxOTs5p7zMM44yfFx4ejsPhICwsjIiIiAtTtIiIuDTdYyUiIq3K/v37WbZsGWFhYYwaNcp5hsnhcJyy3MqVK097r7e3N3D65YFDhw7FYrGwdOnS0z7H4XBQVlbWlKsgIiIuSGesRETEbe3evZusrCzsdjvFxcUcOHCAxMREQkNDeeyxx/Dy8sLLy4vevXvz6aefYrPZCAkJYe/eveTl5Z32ed26dQNg0aJFjBw5Eg8PDwYNGkR4eDjTp0/ngw8+ID8/nyFDhuDj40NeXh47duxg3LhxTJo0qblXX0REmpHh+PGhNRERkRZuw4YNvPbaa86frVYr/v7+REVFMXDgQC655BJ8fX2drxcVFfHWW29x4MABHA4Hffv25dZbb+Xuu+9m6tSp3HDDDc5lly1bxtq1azlx4gQOh+OUBwRv27aNlStXOmcSDA0NpU+fPlx11VW6RFBExM0pWImIiIiIiDSS7rESERERERFpJAUrERERERGRRlKwEhERERERaSQFKxERERERkUZSsBIREREREWkkBSsREREREZFGUrASERERERFpJAUrERERERGRRlKwEhERERERaSQFKxERERERkUZSsBIREREREWkkBSsREREREZFG+v9fYuPlQVkFBgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "eq_prices.plot(label=\"CRR European Priced Option\", figsize=(10, 6))\n", + "OPTION_MID.plot(label=\"Option Midpoint\", figsize=(10, 6))\n", + "\n", + "plt.title(f\"CRR Priced Option vs Market Midpoint for {symbol} {right}{strike} expiring {expiration}\")\n", + "plt.xlabel(\"Date\")\n", + "plt.ylabel(\"Price\")\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dba0a09b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 119, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA5gAAAIYCAYAAAAID0PRAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Xd4FFUXwOHfbHrvIRCQQAg1dBAIRXpHinREkCKoIGBBEQRB1M+CoGIFBUUgoUgTQaQIUqUrvZfQ0yE9u/f7Y92VJZuQhIQEOO/z5FGm3tmdmZ0z5xZNKaUQQgghhBBCCCHuka6wCyCEEEIIIYQQ4uEgAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAaYQQgghhBBCiHwhAeZDauDAgWiaxrlz58zTzp07h6ZpDBw4sED3rWkaTZs2LdB95EbTpk3RNK3A92PtuN9++200TeOPP/6467JC5NX9OsfvJOdxZnK/ySwhIYGXXnqJoKAgbG1t0TSNAwcOFFp5hCjq5s6di6ZpzJ07N1frZXUPEOJ+KzIB5rFjxxg5ciShoaF4eHhgb29PiRIl6NChA9999x2pqakWy2uaZvFnY2ODt7c3TZs2Ze7cuSilMu3DFGDd/mdra4u/vz9t27ZlxYoVuS53UFBQpkBO5Ey/fv3QNI0vv/zyrsu2bt0aTdNYtmzZfSiZdUFBQQQFBRXY9h/mHwbTC487r1kfHx+aN2/O/Pnzra4XFxfHxIkTqVGjBq6urjg4OBAYGEj9+vV55ZVX2L9/v8Xyps9Q0zSeeeaZLMuzefNm83K5+U5NwUN2f2+//XaOtyfyJi8PX3K/sXQ/7zdjx47l888/p2rVqowbN45JkyYREBBQ4PvNztChQ9E0DWdnZ+Li4nK83rvvvmu+1o8fP57lcqZz9PY/BwcHypQpw8CBAzly5IjV5fPrBfDFixd54403qF27Nl5eXtjZ2eHv70/Lli359NNPiY+PNy+bmJjI/Pnz6du3LxUrVsTFxQU3Nzfq1KnDtGnTSEtLy3I/R44coWfPnvj7++Po6EiFChWYNGkSycnJWa6zfft22rdvj7e3N05OTlSrVo0ZM2ag1+tzfZx52b+4v6Kjo5k9ezZdu3alXLlyODk54eHhQaNGjfjuu+8wGAxZrpubc+XAgQO8/fbbNGzYkOLFi2Nvb09gYCB9+vRh3759We7j7NmzDB8+nIoVK+Ls7EyxYsVo0KAB3377bbbnflb0ej3Tp0+nWrVqODk54e3tTfv27dm+fbvV5b///nu6dOlCuXLlcHd3x8XFhUqVKjF06NBs7zFZ+euvvxg3bhzt2rUjICAATdMoWbJklstbu1dZe17LLdtcr1EApkyZwuTJkzEYDDRo0IABAwbg6urKtWvX+OOPPxgyZAhfffUVe/bsybTupEmTAEhPT+fUqVMsW7aMzZs3s2fPHmbOnGl1fx4eHowePRqA1NRUDh8+zC+//MJvv/3GRx99xKuvvlpgx1qYAgMDOXr0KB4eHoVdFMD4A79gwQJmz57NCy+8kOVy586dY/369RQvXpxOnTrdxxLmztGjR3F2ds73ZR8mnTt3pkaNGgCkpaVx5swZVq5cyaZNmzhy5AjvvvuuednLly/TsGFDzp07R9myZenXrx++vr7Exsayd+9eZsyYgZOTEzVr1sy0H1tbW5YsWcJnn32Gp6dnpvmzZs3C1taWjIyMPB3HgAEDsnz4L4xM0Y8//khSUtJ93++DRO43hXe/+eWXXyhfvjyrVq0qtDLc7ubNm4SHh6NpGsnJyfz000+MGDHirusppZg9ezaapqGUYtasWXz88cfZrlO9enW6dOkCQHx8PH/88Qc//PADixYtYuPGjdSvXz8/DsnC7NmzGTFiBKmpqVSvXp0+ffrg5eVFdHQ0W7duZfTo0bzzzjtERUUB8Oeff/L000/j7e1Ns2bN6NKlC7GxsaxcuZJXX32Vn3/+mQ0bNuDo6Gixn127dtG8eXPS09Pp3r07pUqVYuPGjUyZMoUNGzawYcMGHBwcLNZZsWIFTz31FI6OjvTq1Qtvb29WrVrFmDFj2LZtG4sXL87xceZl/w+Krl27Ur9+fYoXL56r9UaMGEHv3r157LHHCqhkubd48WKef/55ihcvTrNmzXjssce4du0aP//8M0OGDGHNmjUsXrw4U82P3J4rw4cPZ9euXdSuXZtu3brh6urKgQMHCA8PZ8mSJURERNCtWzeLdXbv3k2zZs1ITk6mbdu2dO7cmYSEBFatWsWwYcNYunQpa9euzXGtFKUUvXv3ZsmSJVSoUIERI0YQExNDREQETZo0YenSpXTu3NlinZ9++okrV65Qr149AgIC0Ol0HD58mDlz5vDjjz+yfPly2rVrl+PPe8GCBXz66afY2dlRuXJlrl27lu3yNWrUMMdSd/rzzz/ZuHFjrvZvpgrZu+++qwBVqlQptXPnTqvLrFq1SjVt2tRiGqCsFX/r1q1Kp9MpTdPUmTNnLOadPXtWAap06dKZ1lu4cKEClLOzs0pMTMxx+UuXLq0Adfbs2Ryvcz8MGDCg0MoFqCeeeCJHy5YvX14Bau/evVkuM2HCBAWoN998M0/leeKJJ6yeK7lVunRpq+dOdiZNmqQAtWnTpnxd9kFjOh/nzJmTad6ePXsUoBwdHVVycrJ5+uDBgxWgBg0apAwGQ6b1Ll++nOm8MX2GXbp0UYCaOXNmpvViYmKUo6Oj6tq1a5b3g6yYzqWH8TvKi9xc6/lpzpw5WZ5P2ZH7Td6WvVeaphXKeZKVr7/+WgHq5ZdfVvb29qpatWo5Wm/t2rUKUAMHDlQBAQHK19dXpaamWl3WdI4OGDDAYrrBYDDfD29/rslq+dz66aefFKC8vLzUL7/8YnWZrVu3qurVq5v/vX//fvXTTz9lOpaEhARVq1YtBaiPP/7YYl5GRoaqVKmSAtSKFSvM0/V6vXrqqacUoN5//32LdeLj45Wfn5+yt7dXu3fvNk9PTk5WDRo0UIBauHBhjo4zL/sXhWPDhg1q5cqVSq/XW0y/cuWKKlWqlALUkiVLLObl5Vz57LPP1MmTJzPt33RN+Pj4ZDrH27dvrwA1d+5ci+m3bt1SlStXVoDavHlzjo91wYIFClBhYWEWzzN//fWXsre3V35+fiohIcFinduXu926desUoCpVqpTj/StlvJ737dtnPlZABQYG5mobJvXr1890jeVUoQaYZ8+eVXZ2dsrOzk79888/2S6bkpJi8e+sAkyllPmkWLx4cab9ZfVAaTAYlIuLiwIsTua7ySrAND14Xb16VT377LPK399fOTs7qwYNGqgtW7YopYwn8Kuvvqoee+wxZW9vrypXrqwWLVqUaR+3P0z98ssvqkGDBsrZ2Vl5enqqp556Sp04cSLTOtYCTNPxW/sBS0xMVO+9956qXr26cnZ2Vi4uLqp+/fpqwYIFVo87NTVVTZkyRZUtW1bZ29uroKAgNX78eJWSkpKrh86PPvpIAWr48OFW52dkZKjAwMBMLwzWr1+v2rRpo7y8vJS9vb0KCQlRr7/+uoqLi8u0DWsPfKmpqerzzz9X7dq1M3/+Xl5eqkWLFurXX3+1WHbTpk3m8+3Ov9s/S2vHndVD3J3Lms4ja39KKdW7d28FqD/++MPq57RkyRIFqBdffNHqfJP3339fAWrGjBlW51+6dEnZ2Nio2rVrm6clJCSoKVOmqCpVqig3Nzfl6uqqypYtq3r27Kn27NmT7f5MsgswlVLK29tbAer69evmaaaHh/379+doH0r993l//fXXqmTJkqpGjRqZlvn0008VoH799df7EmDm9lrJ7uWQ6VycNGmS1XKZmF6YjR492mqZUlJSlKenpwoICFDp6elKKaXi4uLUhx9+qJo1a6YCAwOVnZ2d8vX1VZ06dVLbt2+3up2srvX09HT1xRdfqHr16ik3Nzfl5OSkatSooT7//PNMDxm335fOnj2revXqpXx8fJSDg4OqXbu2WrVqldVjtfZ3txdqcr8xul/3m6y+q9vLotfr1VdffaXq1KmjXFxclLOzs6pTp4768ssvM50rtx/LlStX1ODBg1WJEiWUTqfL1cuG2rVrK51Opy5cuGAORrJ6wX0707Lbtm1Tr7zyigJUeHi41WWzCxh37dplfqGdk+VzKiEhwXwv/e2337Jd9s5nqqzMnz9fAapjx44W0zds2KAA1aRJk0zrnD592nxvvf3l4HfffacA9cwzz2RaJ7vtWZOX/d/N0aNH1YABA1TJkiWVnZ2d8vf3V3369FHHjh2zWO6ll15SgBozZkymbcyePVsBqmXLlubz9/Z73NGjR1Xnzp2Vl5eXcnZ2Vg0bNrT6XWX1Es304ik+Pl6NGTNGlS5dWtna2pp/E+52D7hx44YaOnSoCggIMD93fv/991Y/j5SUFDVp0iRVpkyZe3rOy44pyTRixAiL6fl5riilVEhIiAIyPbNUrFhRASomJibTOiNHjrQa/GancePGClAbN27MNK9///4KyPLztsbT01PZ2dnleHlr8hpg/v333+Z1MzIycr1+oVaRnTNnDunp6fTu3ZvQ0NBsl81LNQc7O7s8lSuv690pLi6Ohg0b4ubmRp8+fYiJiSE8PJw2bdqwY8cOhg0bRkxMDB07diQ9PZ2FCxfSq1cvSpUqZbXazM8//8yaNWvo2rUrTZs25cCBAyxdupRNmzaxfft2KlSokOdyNm/enP3791OrVi0GDRqEwWDgt99+o2/fvhw+fJipU6eal1dK0bNnT1asWEFwcDAjRowgLS2N77//nn/++SdX+x4wYADjx49n4cKFTJs2LVM1rjVr1nDp0iVatWpFmTJlAPjmm294/vnncXFxoUePHvj7+/PHH3/wwQcfsGrVKrZt22a1WuTtYmJiGDVqFGFhYbRq1Qo/Pz+uXLnCqlWraN++PbNmzWLIkCGAsS3UpEmTmDFjBoC5ejVgru55r0aPHs3y5cvZvHmz1eqXzz//POHh4Xz77bc88cQTmdb/5ptvAGMVkez079+f8ePH8+OPPzJq1KhM83/66Sf0er25HZBSirZt27J9+3YaNGjAkCFDsLW1JTIykk2bNtG4cWNq166dt4P+1759+4iJiaF06dL4+fmZp/v4+ABw4sSJXH/ONjY2DBo0iClTprBnzx7q1Kljnjdr1izKlClDy5Yt76ncOZGf10pudOnSBQ8PDxYsWMBHH32Era3lrX7FihXExcXxyiuvmOcdPXqU8ePH06RJEzp06ICXlxcXLlxg5cqVrFmzhlWrVtG2bdu77js9PZ1OnTrx22+/UaFCBfr27YujoyObNm1i5MiR7Nq1i3nz5mVa7/z58zz++OOULVuW/v37m6sVde7cmfXr19OsWTPA2J7X09OTFStWWFS5Bu563cv9BvM278f9ZuDAgTRt2pTJkydTunRp833l9v3179+fBQsWUKpUKYYMGWJu+/rCCy+wdetWq+2zY2JiqF+/Pq6urnTr1g2dTkexYsVydOz79+9n7969tGrVilKlSjFw4ECWLl3Kt99+S7169bJc79q1a6xcuZLy5csTFhaGu7s706ZN49tvv6VXr1452reJ+rePiPzuDGrJkiXmz6Z169bZLpvTZyrT89Cd95CNGzcCWL0nlC1blvLly3PixAnOnDlDcHDwXddp0qQJzs7ObN++ndTU1LuWLy/7z87atWvp1q2b+f5Vrlw5IiMj+fnnn1m9ejWbNm2iVq1aAHz00Uds3bqVGTNm0KJFCzp06ADA4cOHeemllwgICOCnn35Cp7Ps5uTs2bM0aNCAqlWrMmzYMK5cuUJERATt2rVjwYIFOT6P0tLSaN68OTExMbRu3Rp3d3fz/So7pmdSe3t7unfvTmpqKosXL2bQoEHodDoGDBhgXlYpxVNPPcXq1asJCQlhxIgRpKenM3fuXA4fPpyjcuZEXs6v3J4r2e2nSpUqHDt2jNWrV/P000+bpyclJbFx40acnZ1p0KBBjo4lJSWF7du34+zsTOPGjTPNb9euHfPmzWPjxo08++yzd93e1q1biYuLM59399u3334LwODBg/PUBrNQM5jNmzdXgJo1a1au1yWLDObmzZuVTqdT9vb26vLlyxbzsstgzps3TwHKz88vy3S1NdllMAE1bNgwi7ewP/74owJj9ZWOHTta7GvLli0KjNX7bmd6kwVkeps/Y8YMBajmzZtbTM9NBtO07AcffGAxPTk5WbVp00ZpmmaRRTK90axfv75F+aOjo1XZsmVz/WarZ8+eWWa3nnzySQX/ZaPPnTun7O3tlZubmzp69KjFss8//7wC1NChQy2mW8sopKSkqIsXL2baX1xcnKpSpYry8vJSSUlJFvPuVmXN2nHnNKOQ3bImVapUUQ4ODioqKspi+unTp5WmaSosLCzLst2udevWCrBaa6By5crK3t7evA/TG6w7z0mljJkHa2/9rDGdY507d1aTJk1SkyZNUuPGjVN9+vRRLi4uqmTJkubMvsnnn3+uAOXm5qZee+019fvvv2c69juZPsNZs2apc+fOKZ1Op5577jnz/B07dihATZ06VaWnp+c5gzlgwADzcdz5d+XKFfPyeblW8iODqZRSzz33nNV7hlL/VQv6+++/zdPi4uLUjRs3Mi178eJFVbx4cVWxYsVM87I7j0eMGGHx1jMjI0MNGjRIAWr58uXm6ab7EqDefvtti22ZqiS2a9fOYnpeq8gqJfebuy1rkl/3m6z2r9R/1clq1qypbt68aZ5+69YtVbt2bQWo+fPnZ9oWoPr372/OvufGsGHDFGCunZOenq4CAgKUi4uLio+Pz3I9U+2P9957zzytdu3aStM0q9Xysqsi+8wzz2T63c6PDKbp+ho/fnyet3Gntm3bKjDWCrld9+7ds83udOjQQQEWGfo6depYzSKZVKlSRQHqyJEjdy1XXvaflZiYGOXp6al8fHzU4cOHLeb9888/ysXFRdWsWdNi+smTJ5Wbm5vy9fVVkZGRKjExUVWpUkXpdDq1fv16i2Vvv8e9+uqrFvN2796tbG1tlaenp8X5l10GE1AtWrRQt27dynQs2d0DADV48GCL+/Lhw4eVjY1NpmqYpmfVxo0bW1QrjY2NVRUqVMiXDGZ6eroKDQ1VgFq7dq3FvPw8V0y/+9YycUePHlXFixdXNjY2qlOnTur1119Xzz//vCpVqpQqXrx4js4fk0OHDilAhYaGWp2/e/duBajHH3/c6vzFixerSZMmqbFjx6ouXbooe3t75e3tnWUNopwyHXtuJCUlKU9PT2VjY6MuXLiQt/3maa18YqoCt2bNmlyva7pYTA91b775purZs6eys7NTmqapzz77LNM6povcw8PDvN4bb7yhOnbsqDRNU/b29urnn3/OVTmyCzCdnZ0z1bXOyMhQtra2ClCnT5/OtL2goCAVFBRkMc10o7kziDRtLzg4WAHq3Llz5uk5DTCjoqKUjY2NqlOnjtXjO3DggALUa6+9Zp7WsmXLLKsAmMqamxvP+vXrFaAaNmxoMf3y5cvK1tZW+fv7q7S0NKWUUlOnTlWAGjduXKbtxMTEKDc3N+Xo6GhR/Se3baKmTZumIHO9+8J+4Js5c6aCzG1h3njjDQWoH374IbvDMjMFPdZ+6ADVtWtX8zRTgNmnT58cbTsrpvPR2p+Tk5MaO3asio2NtVjHYDCocePGKUdHR4vlg4KC1JAhQ9SBAwcy7ef2AFMp48ORm5ub+Yd40KBBysbGRl26dOmeAszs/m5/GZOXayW/Asxt27YpQHXv3t1i+pUrV5SNjU2mB6bsmKoKnT9/3mL6neXX6/XK29vbourt7WJjY5WmaapHjx7mabe/+LNWDeexxx5TPj4+FtPuJcCU+032y5rk1/0mq/0r9d/1Ya2KoOl7atasWaZt2dvbq2vXruV4/ya3bt1Sbm5uysPDw+KFj6m665dffml1PYPBoIKDg5VOp1ORkZHm6aaXYGPHjs20jukcrV69uvl5Y/To0apGjRrm+96OHTsyLX8vAWa7du0UoL766qs8b+N2puOrUaOG+ZowadWqlQLU77//bnXdvn37WgTySv1XTdFaQK6UUmFhYQrI0QN1XvafFdOLemtt9pVSavTo0QrIFHyamiI0adJEPfvss1kG97c/e975TKjUf/f829sB3i3AtPb7p1T29wBnZ2erL1GaNGmiAIuXPC1atLB6X1LqvzaN9xpgmq679u3bZ5qXX+dKdHS0eVvWmqAppVRkZKS5aqvpz87OTr322ms5fomu1H+/uXf+tpicOHFCAap8+fJW5/fq1cuiDCEhIblqspeVvASYc+fOVYDq0KFDnvdbJHqRvReTJ0+2+LemaXz33XfZpp/j4+Mzrefg4MCKFSto06ZNvpWtfPnyuLm5WUyzsbGhWLFiJCYmUrZs2UzrBAYGsmvXLqvbs1ZVycbGhkaNGnH69Gn2799P6dKlc1XG3bt3o9frsxxeIT09HTBWnzPZt28fOp2ORo0aZVo+Lz1oNm/enODgYLZt28bRo0epVKkSYKxCnZGRwcCBA83VG0xdTTdv3jzTdry8vKhZsyZbtmzh2LFjVK9ePdv9Hj58mI8++ogtW7Zw5coVUlJSLOZfunQp18dSkJ555hneeOMNvv32W1555RUAc5UVLy8vevbsmaPtdO3aFQ8PD+bPn8///vc/c9WHH374AcCim/zKlStTo0YNFi5cyPnz5+ncuTONGjWiTp062Nvb5/oY5syZY96+Xq8nMjKSH374gbfffpsVK1awZ88eXF1dAeO1/N577zF27Fh+++03du7cyb59+9i1axezZ89mzpw5fPXVVwwdOjTL/Q0dOpS1a9cSHh5Ojx49iIiIoEOHDpQoUSLPPcgCbNq0KUfnen5fK7kRFhZm7rkzNjYWLy8vAObPn29RDfp227Zt49NPP2XHjh1cv349Uxftly5dyrZ3whMnThATE0NISIhFtfrbOTk5WdxPTGrUqGG1Gk6pUqXYsWNHdoeaK3K/yZn8ut9kx3R9WLsWnnjiCWxsbDINRQTGKrb+/v653l94eDg3b95k2LBhFj2iDhw4kGnTpjFr1iyef/75TOtt3LiR06dP06ZNGwIDA83T+/btyyuvvMLcuXOZOnWq1eY1Bw8e5ODBg4Cxml7x4sXp378/b7zxBpUrV871MdwvP//8M6NHjyYgIIClS5fmW9Ohosh0fzl48KDV56ATJ04Axueg27+z3r17s2HDBmbPns2WLVto1KhRpmfL29WqVSvTMyEYfwt++OEH9u/fb1FNNSuOjo5Uq1btrsvdKSQkBHd390zTS5UqBUBsbKz593f//v3odDrCwsIyLW/t9yy3PvvsM6ZNm0bFihWtNpnID4mJiXTu3JmTJ08yduxYevTokWmZ/fv306VLF/z9/fnzzz+pUaMGcXFx/PTTT0yYMIHly5eze/du8+gLM2bMyDSsUZcuXfKl+UJ4eDjh4eEkJCRw6NAhJk+eTMOGDfnmm28sfq+tnaMDBw7M12GtTNVjhw0bludtFGqAWbx4cY4ePXpPP6zq37YMiYmJ7Nixg8GDBzN8+HBKly5t9aEAoHTp0uZxKxMSEvj9998ZMmQIPXv2ZMeOHfl2089qOBBbW9ts52X14JtVGxPTeGK3j2uVU9HR0YAx0Ny9e3eWy926dcv8//Hx8Xh7e1v9wcnL2GaapjFkyBDGjRvH7NmzmTZtGkopvvvuOzRNswggTMeYVdfdpul3G9ds586dNG/enIyMDFq0aMGTTz6Ju7s7Op2OAwcOsGLFikxjrxY2Nzc3nn76ab7++ms2bdpEs2bNWLlyJVevXmX06NGZupDPipOTEz179mTWrFmsW7eOdu3akZaWxsKFC/Hz87PojtrGxsbc7fuSJUt4/fXXzWUZMGAA77//vvkHKbdsbGwoXbo0EydO5MSJE8yfP5/PP/+ccePGWSzn6elJr169zO1TEhMT+d///sfUqVMZOXIkTz75ZJbXRqdOnShWrBizZ88mPT2dxMTEbAPS/Jbf10pumdochoeHmx+cf/jhB+zs7Ojbt6/FssuWLaN79+44OjrSqlUrgoODcXFxQafT8ccff7B58+a7XhOm+8nJkyezfdC6/X5iklU7Rltb22zHScstud/kTH7db7Jjuj6svayytbXF19eX69evZ5qX12vH9NB058uV0NBQateuzd69ezO12c5uPW9vbzp16sTSpUtZsWIF3bt3z7TPAQMG5Gq81nthOh/v9WXF8uXL6d27N/7+/mzatMnqy3DTM0xWzx2m6bdf13lZJyv5uS3TfWvWrFnZLmftvtW9e3dmz54NwMiRI7Ntq5Zfz3D+/v55ar+b3T0WsBhb0nRt3tlmEbI+jpyaOXMmo0aNonLlymzYsAFvb+9My9zr95uYmEiHDh3YunUrL7/8Mh988EGmZTIyMujZsyc3btxg165d5u/B1dWVN954g2vXrjFjxgymT59uDupmzJjB+fPnLbYTFBREjRo18u2cdHd3JywsjFWrVlGnTh2ef/55WrZsaR7L0tpva9OmTfMtwDx8+DDbt2+nZMmStG/fPs/b0d19kYJjeguyYcOGe96Wi4sLLVu2ZNWqVej1egYMGJCjceHc3d156qmn+Omnn0hISOCZZ54xB61FTVZj2Vy9ehXIOqDNjmmdMWPGoIxVpq3+bdq0yWKdmJgYc3bTWlly69lnn8XOzo4ff/yRtLQ0Nm7cyJkzZ2jWrBnlypXLVN6s9nPlyhWL5bIydepUkpOTWbduHWvWrGHGjBlMmTKFt99+O9uOHgqbKUgwdbJh+u9zzz2Xq+2Y3pKasparV68mOjqavn37ZgqGvLy8mD59OhcvXuTkyZPMnj2bihUrMnPmTKtv+/PC9Jn/9ddfd13WxcWFd955h0aNGpGamsq2bduyXNbOzo5nn32WnTt38u6771KyZMm8jeeUR3m5VkwdQ1h70ZSbAeHB2IGKTqczf8/79+/nn3/+oX379vj6+los+9Zbb2Fvb8+ePXtYvnw506ZNM18TOe1AzHTdde3aNdv7ydmzZ3N1HPlN7jc5k1/3m6xkd31kZGQQFRVlNeOSl4frv//+23x/adCgQaaBxPfu3Qv8F0ya3Lhxg+XLlwPQp0+fTOstXbrU6nqFIT+eqRYvXkyPHj0oVqwYmzdvzvLaN003ZffudPLkScBYkysn62RkZHD27FlsbW2tBrT5sf+smK7fgwcPZnvfujO7GBUVxeDBg3F2dsbZ2ZkxY8Zw48aNLPeTX89w+d05lDXu7u7ExMRY/R2627iK2ZkxYwYjR44kNDSUTZs2Zfmy6F7OlZs3b9KuXTs2b97M2LFjmTZtmtV9HDt2jFOnTlGpUiWr5TB1LGe6N4BxnOQ7zwvTi6fg4GBsbGw4c+aM1c8tN+ckgL29PS1atCAlJYWdO3eap1s7N/OzRtQ9d+7zr0INME0/8kuXLuXIkSPZLpvTt7vVqlVj6NChREZGMn369ByXpUOHDrRt25a9e/eyYMGCHK93P23evDnTNL1ez9atWwGsDjh/N48//jg6nY4///wzx+vUqlULg8Fg3u/t/vjjj1yXAYxvxJ588kmioqJYvny5+Y3gnQ8ypmO0tp+4uDgOHDiAo6OjudpbVk6dOoW3t7fVi9La5wzGjNvtb/jym+lCzm4f1apVo2HDhixbtoxdu3axfv16mjRpctfjvVPDhg0JCQlhxYoVxMfHmwOQu1XPKVeuHIMHD2bz5s24urqyYsWKXO03K7GxsQC5ylSZqhrd7YWQqWfKyMhIBg0adE83zNzKy7Viqsp68eLFTPP27NmTq/2XKlWK5s2bs2vXLo4fP57t93zq1CkqV66c6VzKqvzWVKxYEU9PT3bu3Gk1aMgvOblWsiP3m/t7v8lKzZo1MRgMbNmyJdO8LVu2oNfr860HRdNDU9OmTRk8eLDVPycnJxYuXGiRqfrhhx9IS0ujdu3aWa7n5+fH+vXrC/3FSffu3fH29mbHjh2sX78+22WtPVPNnz+fPn36UKJECTZv3kxISEiW65tqiK1duzbTvDNnznDixAlKly5tEQBkt86WLVtISkoiLCwsR72C5mX/WTH12p+b5yBTwHnp0iU+/fRTPv30Uy5fvpxtkmLfvn3cvHkz03TT/SUvz3AFxXRtbt++PdO8nP4e3OmDDz5gzJgx1KhRg02bNmVbzT2v50p8fDytW7fmzz//ZPz48VYzlyamayAqKsrqfNPLgpw2B3J0dCQsLIykpCSr59KaNWsA600usmKqjWAtk1wQUlJSmDdvHjY2NgwePPjeNpbn1pv5xDQGTlBQUJaNWdesWWO1oX9WxY+MjFQODg7K09PTooFudr3IKvVfJyfBwcE57p3ubuNgZrVOVmWw1kFETnqRvfPzyU0vsqaxeaZMmWK1k41Tp05ZjAln6iSmQYMGmXrGNHU4lJfG36YeIx9//HHl4OBgdRBr09ipHh4emRp/jxgxQgFqyJAhFtOtfaZt2rRRgDp48KDFdNMYVlhpXF+3bl3l4OCQqbdHE2vHnZtON7744gsFdx8jydTAPjAwMMedGFhj6sDkvffeU3Z2dlYHGz9z5ozVzqguXbpkHicsJ7IbBzMmJkYFBQUpQH3yySfm6R9++KE6dOiQ1e39+eefytHRUdna2qpLly6Zp9/ZyY/Jb7/9ppYtW2bRI+a9dPKT03Ew83KthIeHK6x0rPT3338rV1dXRQ47+TExnS+vvfaa8vf3V76+vpk67FBKqQoVKig3NzeLz9NgMKi33nrLfE3k5Dw2LT98+HCr18rly5ctOsvIbnzerI5t9erVClATJ060uk5OyP3m/t1vsvpNMF0fdevWVYmJiebpiYmJqm7dugpQP/30U462lZ3be0S8/fy+09NPP60A9e2335qnlS9fXgFq165dWa43YcIEBag333zTPC23nfbkRyc/Sv33fXl7e2fqmdNkx44dmTr5mjt3rtLpdKpMmTIWHQZmJSMjw9xR4+2DsOv1enMPr++//77FOvHx8crX11fZ29tbPO8lJyerBg0aKEAtXLjQYp3ExER19OjRTB2M5WX/WYmKilKenp7Kz8/P6ves1+szXVMff/yxAlSvXr3M00ydtNzZI39OepH18PDIcS+y2f1m5eYeYGLtedHUycudvcjGxcXlqRfZKVOmKEDVrl1bRUdH33X5vJwrMTEx5t5nJ0+efNd9mMaDtvbMEBsbax4j84svvsjhUf7XM3ZYWJjFb/5ff/2l7O3tlZ+fn8X3HBUVZfUZSymlVq1apWxtbZWrq2uuOhu6k+n+nROm3oPvHPc2Lwq9k58333yTjIwMJk+eTN26dQkLC6NOnTq4urpy7do1tmzZwsmTJzO1ichOYGAgw4cP59NPP+XDDz/k/fffz9F6derUoXPnzqxYsYLvvvvunhq3FoROnTrRtWtXunbtSrly5Thw4ABr1qzB29ubL7/8Ms/bnTlzJidPnmTixInMmzePRo0aUaxYMS5fvszRo0fZvXs3CxcuNI+z1KdPHyIiIli5ciWhoaF07tyZ9PR0lixZQt26dTl9+nSeytG6dWuCgoLM1ZhGjBiR6c1RUFAQM2bM4MUXX6RWrVr07NkTPz8/Nm/ezI4dO6hYsWK2b6xMRo8ezW+//UajRo3o2bMnHh4e7Nmzh61bt9K9e3eWLFmSaZ0WLVqwe/du2rZtS5MmTXBwcKB69ep06tQpT8d7p2bNmqHT6Rg3bhyHDh0yZ7ImTJhgsVyPHj0YM2YMly5dwtfXl27duuVpf/3792fixIlMmjSJ9PR0q1mtgwcP0q1bN+rWrUulSpUoUaIEN27cYMWKFaSnp5vbZObU8uXLze2fTZ38rFq1iujoaOrWrWsxrt78+fMZO3YsFStWpH79+hQvXpzExEQOHz7Mxo0bUUoxbdo0SpQocdf93m1MuNyaO3dulhnIGjVq0KVLFyBv10rnzp0JCQlh4cKFREZGUq9ePS5cuGAe93HRokW5KmvXrl1xd3dnxowZpKenM3LkSKttQseMGcPw4cOpWbMmTz31FHZ2dmzbto0jR47QqVMnVq1alaP9vfXWWxw8eJCvv/6aVatW0bx5cwIDA7l+/TonT55k27ZtvPvuu/fU1r1BgwY4OzszY8YMoqOjzdWbRo4cmeNqZnK/ub/3G2v69u3LihUrWLRoEVWqVKFLly5omsby5cs5e/YsvXr1ol+/fve8n4iICOLi4ujUqVO294shQ4bw008/8e233zJ06FD++OMPTpw4QdWqVXn88cezXG/w4MG8++67zJkzh8mTJ99TtmHr1q1WO+ACY42Il156Kdv1+/XrR3JyMiNGjKBt27bUqFGDsLAwvLy8iI6OZseOHRw8eNCiivymTZvM4183a9aMOXPmZNqup6enxZisNjY2zJkzh+bNm9O9e3e6d+/OY489xoYNG9izZw8NGzZkzJgxFttwd3dn1qxZdO/enaZNm9K7d2+8vb1ZuXIlx48fp3v37pnGgvzrr79o1qwZTzzxhMU9Ny/7z4qPjw9Lliyha9eu1K9fnxYtWlClShU0TePixYvs2LGD6Ohoc8dcu3fvZty4cZQpU8ZcbRyMWfLdu3ebxxO+czzzJk2aMHv2bHbt2kXDhg3N42AaDAa++eYbq9XBC8szzzxDeHg4a9euJTQ0lCeffJL09HSWLl1K3bp1OX78eKZxPrPyww8/MHHiRGxsbGjcuDGfffZZpmWCgoIszvu8nCvdunVjz549BAcHYzAYrHaGc3uHPA4ODsyYMYNnn32WoUOHEh4eTs2aNYmNjWXlypXcuHGD+vXr5yqT17t3b37++WeWLFlCzZo16dSpE9HR0URERKDX65k1a5bF93zx4kVq165NnTp1qFChAoGBgebaMTt37sTOzo7Zs2eb7885cezYMf73v/9ZTIuNjbX4fD/++ONMzWTgv5oe+dIM4p5D1Hxy5MgRNWLECFWlShXl5uam7OzsVEBAgGrbtq2aPXu2RTfwSmWfwVRKqatXrypnZ2fl7Oysrl69qpS6ewZTKeOwHJqmqcDAwByNh3k/M5hz5sxRq1atUvXr11fOzs7Kw8NDdevWTR0/fjzTdnKTwVRKqdTUVPX555+rBg0aKHd3d2Vvb69KlSqlmjdvrqZPn55pLLTU1FQ1efJkVaZMGWVvb69Kly6t3nzzTZWSkpLnDKZS/2XVAHXs2LEsl/vtt99Uq1atlKenp7K3t1fBwcHqtddeyzTUhVJZZ3dWrVql6tWrp1xdXZWHh4dq1aqV2rx5c5ZvDm/duqWGDx+uAgMDlY2NTabP0tpx5/Zt4rx581T16tUthuawxtRt+p1vQ3PL1BW5ra2t+Tq53cWLF9W4ceNUWFiYKlasmLK3t1eBgYGqbdu2uRofKqthStzc3FTdunXVhx9+mOl627dvn3rnnXdUs2bNVFBQkHJ0dFQODg6qbNmyqm/fvurPP//MtJ+sMpjWFNQwJXdeX3m5Vi5cuKB69uypvLy8lKOjo6pTp45aunRproYpud3gwYPN5ctqXDGljPea6tWrK2dnZ+Xj46O6dOmi/v7771yfxwaDQf3444+qefPmysvLS9nZ2akSJUqohg0bqnfffddiXK28ZDCVMtZsqV+/vnJxcTEfm7WhXbIj95v7c7/J7jdBr9erL774QtWuXVs5OTkpJycnVatWLTVz5kyLMaRzsq2smIY0uD3TlRVTxnL//v3moS4+/fTTu65nGjbDNNRZXjOY2f117tw5R9tSyngPGTt2rKpZs6by8PBQtra2ytfXVzVt2lRNnz7darYsu7+s7pOHDx9W3bt3Vz4+Psre3l6FhISoiRMnZpl5V0qprVu3qnbt2ilPT0/l6OioQkND1SeffGK1BpXpnpfVd56X/Wfl7Nmz6sUXX1TlypVTDg4Oys3NTVWoUEE9/fTTatmyZUopYwavTJkyys7Ozmq2c/fu3cre3l4FBQWZ7w+33+OOHDminnzySeXp6amcnJxUWFiY1UxzYWcwlTJmC9966y0VFBRk8dsVGRmZq/PRVKbs/rIqW27OFdPzeHZ/1mpRbd68WXXt2lUFBAQoW1tb5eLiomrVqqXef//9HMUBd0pPT1effPKJCg0NVY6OjsrT01O1a9dObdu2LdOyMTExavz48apRo0YqICBA2dnZKWdnZ1WxYkU1bNiwHI3zeSfTNZPdn7XfyiNHjihAlSxZ0urnm1uaUkW0RxthNnfuXJ599lmLIR7Eo61p06Zs2bKF48ePZ9tORhRtmqZlejMvRFEj9xsh8u7cuXOUKVPmvvYoXJB+//13WrduzRtvvJHjGoLi0VOonfwIIXLvr7/+YvPmzbRp00Ye9oQQBUruN0I8mi5fvpxpWnR0NG+88QZgbH4hRFYKvQ2mECJnvvrqKy5dusScOXPQ6XTZjjMohBD3Qu43QjzaXn75ZQ4ePEhYWBh+fn5ERkayZs0aYmJiGDZsWLbtkoWQAFOIB8QHH3xAZGQkZcuWZd68eXJzF0IUGLnfCPFo69atG9euXWPVqlXExcXh6OhIlSpVzMPzCJEdaYMphBBCCCGEECJfSBtMIYQQQgghhBD5QgJMIYQQQgghhBD5QgJMIYQQQgghhBD5QgJMIYQQQgghhBD5QnqRzWexsbFkZGQUdjHEv/z8/Lhx40ZhF0PcI/keHx7yXYqiRM7Hh4N8jw+Ph+m7tLW1xcvLq7CLUSgkwMxnGRkZpKenF3YxBKBpGmD8TqSz5AeXfI8PD/kuRVEi5+PDQb7Hh4d8lw8PqSIrhBBCCCGEECJfSIAphBBCCCGEECJfSIAphBBCCCGEECJfSBtMIYQQQogiKiMjg6SkpMIuRpGWnJxMWlpaYRdD5IMH7bt0dnbG1lbCqTvJJyKEEEIIUQRlZGSQmJiIm5sbOp1UOsuKnZ2ddLD4kHiQvkuDwcDNmzdxcXGRIPMOcrcSQgghhCiCkpKSJLgUoojS6XS4ublJDQMr5I4lhBBCCFFESXApRNEl16d18qkIIYQQQgghhMgXEmAKIYQQQgghhMgXEmAKIYQQQgghhMgXEmAKIYQQQohH2rRp02jVqlVhF+O+6969OxMnTizUMkRERFCpUqV8386d3+no0aMZNGjQPe9H3J0EmEIIIYQQIt+MHj2awMBA81+VKlXo168fR44csVhu/vz5tGzZkpCQECpVqkTr1q35/PPPzfOnTZtGYGAg/fr1y7SPr776isDAQLp3755lOS5evGhRjvLly9OsWTPefPNNzpw5Y7Hs8OHDiYiIuMcjvzf16tVj1qxZ97ydiIgI8zGXKlWKypUr07FjR6ZPn05CQoLFsrNmzWLs2LF52s/ff/9NYGAge/futTq/Z8+eDBkyJE/bvhtrn9WTTz7Jn3/+meU6U6ZMYfr06eZ/F4Xg+mElAaYQQgghhMhXzZo1Y//+/ezfv5+IiAhsbGwYMGCAeX54eDiTJk1i8ODBrFu3juXLl/PCCy+QmJhosZ1ixYqxfft2Ll++bDE9PDycwMDAHJUlPDyc/fv38/vvv/PGG29w8uRJWrVqZRGMuLi44O3tfQ9HnLW0tLQC2W523Nzc2L9/P3v27GHFihX069ePJUuW0Lp1a65evWpezsvLC1dX1zzto1q1alSuXNlqYH7x4kW2b99O796983wMueXk5ISvr2+W893d3fHw8Lhv5XmUSYAphBBCCPEAUEqhUlPu/59SuS6rvb09/v7++Pv7ExoayogRI7h8+TLR0dEArFu3jk6dOtGnTx/KlClDhQoV6NKlC2+88YbFdnx8fGjSpAmLFy82T9u9ezcxMTG0aNEiR2Xx8vLC39+f0qVL06ZNGyIiIqhZsyavvvoqer0eyFydcvv27XTo0IFy5cpRqVIlOnfuTGRkpHn+unXraN++PWXLliU0NJTBgweb59WrV4/p06fz0ksvUaFCBXOG8K+//qJr164EBwdTp04d3nrrLfMYit27dycyMpK3337bnH00yW69rGiahr+/P8WKFSMkJIQ+ffqwYsUKEhMTeffdd83L3Z7Fe//99+nYsWOmbbVs2dIi83e7Pn36sHLlSpKTky2mL1q0iGLFitGsWTPi4uJ46aWXqFy5MsHBwTz99NOZMsi3O3fuHM8++yzVq1cnJCSE9u3bs2XLFosyW/us7lbV9vYqsqNHj2bHjh1899135m1cuHCBhg0b8vXXX1usd+jQIQIDAzl79myW2xaWbAu7AEIIkZ2fDlxn79oLTGpaAg8Hm8IujhBCFJ60VAwjet733epmLgIHxzyvn5iYyNKlSwkKCsLLywsAPz8/du7cSWRkJCVLlsx2/d69ezN16lRGjRoFGAOJrl275rk8Op2OIUOGMHjwYP7++29q1qxpMT8jI4PBgwfTt29fvvjiC9LT09m/fz+apgGwfv16hgwZwksvvcSnn35KWloaGzdutNjGN998w+jRo3n55ZcBY9DUr18/xo4dy7Rp04iOjmbChAmMHz+e6dOnM2vWLFq1akW/fv0sqgTfbb3c8PX1pWvXrkRERKDX67GxsfxN7datGzNnzuTcuXMEBQUBcPz4cY4ePZpl1d2uXbsydepUfvnlF3r06AEYX4QsXryYHj16YGNjw5gxYzh79ixz5szB1dWV9957j/79+/PHH39gZ2eXaZuJiYk0b96c119/HXt7e5YsWcKzzz7Lli1bCAwMzPKzyo0pU6Zw5swZKlasyKuvvgoYX2b06tWLiIgIhg8fbl520aJF1K9fnzJlyuRpX48iyWAKIYqs41HJLDoUzemoRHZdvFnYxRFCCJFD69evJyQkhJCQEMqXL8/vv//O119/bR6Y/uWXX8bd3Z169erRuHFjRo8ezcqVKzEYDJm21bJlS27dusXOnTtJSkpi1apV91z1sly5coCxKuedbt68SUJCAi1btiQoKIiQkBB69uxpzpR99tlndO7cmVdffZWQkBCqVKnCyJEjLbbRsGFDhg8fTlBQEEFBQcycOZOuXbsydOhQypYtS926dXnnnXdYsmQJKSkpeHl5YWNjg6urqznzC9x1vbwc961bt4iNjc00r0KFClSuXJlly5aZp/3888/UrFkzy+DKy8uLtm3bWlST3bZtGxcvXqRXr16cOXOGdevW8dFHH1Gv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IiIiIiKioGCCSUREREREREHBBJOIiIiIiIiCggkmERERERERBQUTTCIiIiIiIgoKJphEREREREQUFEwwiYiIiIiIKCiYYBIREREREVFQMMEkIiIiIiKioGCCSUREREREREHBBJOIiIiIiIiCggkmERERERERBQUTTCIiIiIiIgoKJphEREREREQUFFKoD0BvJIn/pFrDmOgD46gfjCVpCc9HfWAc9UMvsdTL4zgTgqIoSqgPgoiIiIiIiMIfS2RJt3p7e/Hggw+it7c31IdCZ4Fx1A/GkrSE56M+MI76wVjqBxNM0i1FUXD48GFwkT68MY76wViSlvB81AfGUT8YS/1ggklERERERERBwQSTiIiIiIiIgoIJJumW0WjE8uXLYTQaQ30odBYYR/1gLElLeD7qA+OoH4ylfnCKLBEREREREQUFVzCJiIiIiIgoKJhgEhERERERUVAwwSQiIiIiIqKgYIJJREREREREQcEEk4iIiIiIiIKCCSYRhZTP5wv1IRAREdEI6+3tDfUh0Chhgklhp729HatXr8bmzZtRW1sLAOBuO+GntbUVDz30EN58881QHwqdJafTiaqqKrS3t4f6UIjQ09MTOBdlWQ7twdBZaW9vx7vvvot169bhwIEDAPh+H45aW1vxyCOP4LXXXoPX6w314dAokEJ9AETD8eabb+KDDz7AhAkTUF1djfj4eNx9991IT0+HoigQBCHUh0hD8L//+79Ys2YNiouLcfHFF4f6cOgsrFixAuvXr0d0dDRaWlpw2223YcaMGTCZTKE+NBqD3nnnHXz00UdYunQprr32WogiP0cPV2+99Rb+8Y9/ID8/Hy0tLejp6cF9992HvLw8vt+HkVdffRUfffQRiouLsXz5ckgSU4+xgFGmsLF+/Xps27YNDzzwAIqKirB792688cYbOHDgANLT0/lmEwaam5vxyCOPwGQy4T//8z+Rl5cX6kOiM9TY2IiXXnoJ7e3t+NGPfgSbzYa1a9fib3/7G9LS0pCZmRnqQ6QxpK+vD3/7299QVlaGhIQElJeXo6SkBPn5+UxGwtD27duxdetW/OxnP0NxcTGqqqrw8ssvY+vWrcjLy2M8w0BnZyfuv/9+KIqCX/ziF8jPzw/1IdEoYoJJmuW/KPD//44dO2C321FUVAQAKCoqwhtvvDEoSeGFhLaJoojY2FgkJSUhLy8P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xEcffYRf/OIXePrpp1lSS0REYYcJJhER0RDExcXBZrOhoaEBAHDRRRfhsssuG3Sb8ePH449//CNKSkpQUFCArKwsjBs3Dl9++SVmzZoVWLUEgKamJqxcuRLXXHMNrrzyysD3Z8+ejQcffBBr1qwZ9H0iIqJwwG1KiIiIhshisaC3txcABvVhut1udHZ2Yvz48QCAw4cPn/Z3bd68GYqiYN68eejs7Az8z+FwIDk5GXv37h2ZB0FERDSCuIJJREQ0RH19fYiOjgYAOJ1OvPXWW/jqq6/Q0dEx6HY9PT2n/V319fVQFAX33nvvCX8uSXyLJiKi8MN3LyIioiFoaWlBT08PkpKSAADPPPMMSktLcfnllyM7OxsWiwWyLOOJJ56ALMun/X2yLEMQBDz00EMQxeMLiiwWS9AfAxER0UhjgklERDQE69evBwAUFxfD6XRi9+7duPrqq7F8+fLAberq6o67nyAIJ/x9ycnJUBQFiYmJSE1NHZmDJiIiGmXswSQiIjqNPXv24J133kFiYiLmz58fWHFUFGXQ7VavXn3cfc1mM4Djy2Znz54NURTx9ttvH/d7FEVBV1dXMB8CERHRqOAKJhER0QDbt29HTU0NZFlGe3s79u7di127diE+Ph4PPPAATCYTTCYTCgoK8I9//AM+nw+xsbHYuXMnGhsbj/t9OTk5AIA33ngD5557LgwGA2bMmIHk5GRce+21eP3119HU1IRZs2bBYrGgsbERX3/9NZYsWYLLL798tB8+ERHRWRGUYz82JSIiGoM+//xzPP/884GvJUlCZGQkMjMzMX36dCxevBhWqzXw89bWVrz00kvYu3cvFEXBlClTcMstt+DOO+/E8uXLcfXVVwdu+8477+Djjz9GW1sbFEXBs88+G9iyZPPmzVi9enVg8mx8fDwmT56Mb33rWyydJSKisMMEk4iIiIiIiIKCPZhEREREREQUFEwwiYiIiIiIKCiYYBIREREREVFQMMEkIiIiIiKioGCCSUREREREREHBBJOIiIiIiIiCggkmERERERERBQUTTCIiIiIiIgoKJphEREREREQUFEwwiYiIiIiIKCiYYBIREREREVFQMMEkIiIiIiKioPj/IbzCj6sXhMwAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bs_discrete_ts = bs_discrete.timeseries\n", + "bs_discrete_ts.plot(label=\"BSM Discrete Div Volatility\", figsize=(10, 6))\n", + "bsm_vols.plot(label=\"BSM Equivalent Volatility\", figsize=(10, 6))\n", + "crr_vol.plot(label=\"CRR Implied Volatility\", figsize=(10, 6))\n", + "plt.title(f\"CRR Implied Volatility vs BSM Equivalent Volatility for {symbol} {right}{strike} expiring {expiration}\")\n", + "plt.xlabel(\"Date\")\n", + "plt.ylabel(\"Implied Volatility\")\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df222403", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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b29vSzkfJPWbmjFjBp9//jlffvklrq6uPP744+nGUKlSJRYvXsyECRPo0qULhmHg6+ubrBRtxIgRvPHGG9SvX5+YmBguXryYqeNSc/v2bapXr47JZMLNzY0yZcrQtWtX+vfvj5ubW7rHtmvXjnfeeQcbGxvatGmT7ti8ymSYu09Jtrh+/TpxcXHWDkNERERERETuQ3h4OO7u7tYOQyRXpfW6t7e3p0iRIhken3oxoIiIiIiIiIiISBYoySQi8hCKiYHff3cgMdHakYiIiIiIyINCSSYRkYdMVJSJHj286NKlMD/9dG/19SIiIiIiIv+lJJOIyEMkNhYGDCjE3r2OAPzyi5JMIiIiIiKSPZRkEhF5SCQkwCuvFGLzZiecnRMxmQwOHHDg4sWUy8eKiIiIiIhklZJMIiIPAcOAN94oyKpVzjg4GHz//S1q144FYNUqJytHJyIiIiIiDwIlmUREHnCGAWPHujNvXgFsbAy+/PIWjRrF0KFDFACrVjlbOUIREREREXkQKMkkIvKA++ILV775xhWAzz4LpW3baADato1WyZyIiIiIiGQbJZlERB5g339fgE8/dQfg/ffD6N49yrKvaNFElcyJiIiIiEi2UZJJROQBtWiRM+++WxCA//0vnP79I1KMUcmciIiIiDyIunbtynvvvWftMHJVnTp1mDlzplVjUJJJROQBtGaNE8OHewDQv/8dhg27k+q45CVz+idBREREJD8bOnQoPj4+lq/KlSvTu3dvjh49mmzcnDlzaNGiBWXLlqVixYq0atWKqVOnWvZPnDgRHx8fevfuneIaX331FT4+PnTt2jXDeFavXk3Xrl2pUKECZcuWpUWLFkyePJlbt27d/83+v127duHj40NYWFiy7TNnzmTkyJHZdh1rMT8XPj4+lC5dmipVqtClSxdmzpxJTExMsrG//vorzzzzjJUiTaJPFCIiD5ht2xx46aVCJCaa6N49ktGjwzGZUh97d8nc6tWazSQiIiKS3zVt2pSDBw9y8OBBFixYgK2tLc8//7xl//z58xk9ejT9+vVj3bp1LFu2jJdeeomIiOSz3osWLcquXbu4dOlSsu3z58/Hx8cnwzjGjx/Piy++yKOPPspPP/3Epk2beO+99zh69CiLFy/OnptNR6FChXB1dc3x6+SG8uXLc/DgQfbu3cuiRYto374906ZNo2PHjty58+8fk728vHB2tu7v9EoyiYg8QP74w55+/TyJjTXRtm0Un3wSik0G7/TmkrmVK5VkEhEREUmVYWBKiLTKF4aRpVAdHBzw9vbG29ubKlWqMGTIEC5dusSNGzcAWLduHR06dKBnz574+/tTvnx5OnXqxJtvvpnsPF5eXjRq1IhFixZZtu3bt4+bN2/SvHnzdGM4ePAgU6dO5b333uPdd9+lVq1alCpVikaNGjFz5kyefvppy9jZs2dTr149/Pz8aNiwIb/88kuyc/n4+DB37lz69etHmTJlqF+/PuvWrQPg/PnzlnNVqlQJHx8fhg4dCqQsl6tTpw5Tpkxh+PDhlCtXjlq1avHzzz9b9qc2I+rIkSP4+Phw/vx5y7bVq1fTtGlT/P39qVOnDl9//XWKeH/77bdk2ypWrMiCBQsAiI2NZdSoUVSvXp2AgABq166dbBZZamxtbfH29qZYsWJUrFiRvn37snjxYo4fP86XX36Z7B7N5XIvv/wygwcPTnaeuLg4qlSpkuw5zW52OXZmERHJVUeP2vHss15ERtrQuHE006bdwi4T7/Jt20bz7rv/lsz5+CTmfLAiIiIi+YgpMYri28ta5dqXG57EsHW5p2MjIiJYvHgxfn5+FCpUCIAiRYqwZ88eLly4QMmSJdM9vkePHnz44Ye89tprACxYsIDOnTtneN2lS5dSoECBZDOo7lawYFLf0DVr1jB69GjGjBlDw4YN2bBhA8OHD6d48eLUr1/fMn7SpEm88847vPPOO8yaNYshQ4bw+++/U6JECWbOnMmAAQPYtm0bbm5uODmlvaDNjBkzeP3113nllVdYvXo1b731Fo8//jiBgYEZ3hPAX3/9xeDBgxk+fDhPPvkk+/fv5+2336ZQoUJ07949U+f4/vvvWbduHV9//TU+Pj5cunQpxWyxzAgMDKRp06asWbOGN954I8X+zp07M2jQICIiIihQoAAAW7ZsISoqiieeeCLL18sszWQSEXkABAfb0quXF2FhNtSsGcu3397C0TFzx6pkTkREROTBsWHDBsqWLUvZsmUpV64c69ev5+uvv8bm/6e3Dx8+HHd3d+rUqUPDhg0ZOnQoK1asIDEx5R8aW7RowZ07d9izZw+RkZGsXLmSHj16ZBhDcHAwpUuXxt7ePt1xX3/9Nd26daNPnz6UKVOGQYMG8cQTT6SYHdStWzc6deqEv78/b775JhEREfz555/Y2tri4eEBQOHChfH29sbd3T3N6zVr1ow+ffrg7+/Pyy+/jKenJ7t27crwfsy++eYbGjRowLBhwyhTpgzdu3fnhRdeSBFvei5evIi/vz+1a9emZMmS1K5dm06dOmX6+LsFBgYmm2V1tyZNmuDi4sKaNWss25YtW0arVq1ytIxQM5lERPK5S5ds6NHDi+vXbalUKY4ff7yBi0vWplW3bx/N7787snKlMwMHplyFTkRERORhZtg4c7nhSatdOyvq1avHuHHjAAgLC2P27Nk888wzrF69mpIlS1K0aFFWrlzJP//8w549e/jjjz8YNmwY8+bNY86cOZZkFIC9vT1dunRhwYIFnD17loCAACpVqpRxzJks8Tt16lSK5uK1atXiu+++S7atYsWKlv93cXHBzc2NkJCQTF3jbnfHbjKZKFKkiKWMMDNOnjxJ69atU8T77bffkpCQgK2tbYbn6NatGz169KBhw4Y0bdqUFi1a0Lhx48zfxF0Mw8CURvNVOzs7OnTowNKlS+natSuRkZGsXbuW6dOn39O1MkszmURE8rEbN2zo2dOLCxfs8PePZ+7cGxQsmLUEE0DbtlFaZU5EREQkLSYThq2LVb7SXMElDS4uLvj7++Pv70+1atX47LPPiIyMZM6cOcnGVahQgT59+jB16lTmzZvHtm3b2L17d4rz9ejRg1WrVjF79uxMl4QFBARw7tw54uLishR7Wv47I8pkMqU68yojdv/pJXH3eczJtbsTZPHx8Vm+hslkSpFku/txqFq1Knv27OH1118nOjqawYMHM2DAgCxfB5KSXqVLl05zf+fOndmxYwchISH89ttvODk50bRp03u6Vmbpk4SISD4VHm6id29PTp2yp0SJeBYsuEGRIvfWT6lYMZXMiYiIiDyITCYTNjY2REdHpzmmbNmkflORkZEp9pUvX57y5ctz/PjxTPVjAujUqRMRERHMnj071f3m5tqBgYHs378/2b59+/ZZ4skMcwIqISEh08ekxsvLC4Br165Ztv3999/JxpQtW5Z9+/Yl27Zv3z4CAgIss5i8vLy4evWqZX9QUBBRUVHJjnFzc6Njx458+umnfPXVV/z666/cunUrS/GeOnWKLVu20LZt2zTH1KpVixIlSrBixQqWLl1K+/btMyxhvF8qlxMRyYeiokz06ePJ4cMOeHklMG/eDXx87u8fVnPJ3KpVKpkTERERya9iY2MtiZKwsDBmzZpFREQELVu2BODNN9+kaNGiNGjQgOLFi3P16lW++OILvLy8eOyxx1I958KFC4mLi7M07M5IjRo1eOmllxg7dixXrlyhTZs2FCtWjODgYH766Sdq165N//79efHFFxk8eDCVK1emYcOGrF+/njVr1jB//vxM32/JkiUxmUxs2LCB5s2b4+TkZGl0nRV+fn6UKFGCiRMn8sYbbxAUFMSMGTOSjRk0aBBt27Zl8uTJPPnkk/zxxx/MmjWLjz/+2DKmfv36/PDDD9SsWZOEhAQ++uijZImdGTNmULRoUapUqYLJZGLVqlV4e3un+9gmJCRw7do1EhMTuXXrFrt37+aLL76gcuXKvPjii+neV6dOnfjpp58ICgrK0VXlzJRkEhHJZ2JjYeDAQvz+uyPu7onMnXuDwMD7SzBBUsnce++588cfWmVOREREJL/avHkz1atXB8DV1ZXAwEBmzJhBvXr1AGjYsCHz58/np59+4tatW3h6elKjRg0WLFiAp6dnqud0ccn66najRo2iatWqzJ49m59++onExER8fX1p164dTz/9NABt2rTh/fffZ8aMGYwePZpSpUoxadIkS6yZUbx4cf73v/8xbtw4hg8fTteuXfn888+zHK+9vT3Tp0/nrbfeomXLljz66KOMHDmSQYMGWcZUrVqVr7/+ms8++4wvvvgCb29vXn/99WRlhO+99x7Dhw+nc+fOFC1alLFjx3L48GHLfldXV6ZPn05wcDC2trY8+uij/PTTT8l6Yf3X8ePHqV69Ora2tri5uVGuXDmGDBnCc889h2MGq/106dKFKVOmULJkSWrVqpXlxyWrTEZmO3JJply/fj3b6k5FRP4rIQGGDCnEihXOODklMn/+TWrVis2283fp4sXvvzsyenSYZjOJiIjIQy08PDzdlcpEHkRpve7t7e0pUqRIhserJ5OISD5hGPDWWwVZscIZe3uD7767la0JJkgqmQNYtUp9mUREREREJGuUZBIRyQcMAz780J05cwpgY2MwdeotmjSJyfbrmFeZM5fMiYiIiIiIZJY+QYiI5ANTp7ry9deuAHz6aSgdOqS9Osj90CpzIiIiIiJyr5RkEhHJ4374wYUJE5LqokePDqNHj6gMjrg/KpkTEREREZF7oSSTiEgetnixM6NG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R45gGAaBgYH3eDdiZk4y1agRx88/32DdOifmzHFh61Yny5d5dlPv3pGULq3ZTdZmLpMLDIzjf/9LvUwuLa6uBjNn3uKLL+KYODEpGfPPP3bMmHELb+/8UVKVni+/dGXatKSZeePHh9GxY7SVI7KevFgyl9lZS6kxbw8OtsUwQBNURURExMwUFUXxsmWtcu3LJ09iZKL069atW2zevJk33ngj1VKxtKp4EhMTWbNmDWFhYenOTgoPD2fFihUA6Y5bunQpZcqUoXXr1in2mUwmS6uab7/9lhkzZjBhwgQqV67MggULeOGFF9i0aZPlcznAhAkTePfddy0tal5++WV27txJzZo1ef/99/nss88sybECBQqkGdekSZN45513eOedd5g1axZDhgzh999/p1ChQmkec7c1a9YwevRoxowZQ8OGDdmwYQPDhw+nePHi1K9fP1Pn+PTTTzlx4gQ///wznp6eBAcHEx2d9c8TDRo0oFKlSqxZsyZZiaNZly5dmD59Om+//bal6mrFihUULVqUOnXqZPl6WZGnZjIBtG/fno0bN7JlyxYuXLjAt99+S0xMDE2aNAFg2rRpzJ071zJ+6dKl/PXXX1y9epULFy6wcuVKtm/fbqk1jY6OZu7cuZw4cYLr168TFBTE9OnTuXnzJnXr1gWgZMmSVKtWjRkzZnDq1Cn++ecfvv/+e+rVq4enp2euPwYPmqCgpCRTmTLx2NtDu3bRzJ17k127rjJkyO3/zG7yplcvT379VbObrCWt1eSywsYGhg27w6xZN3FzS7T0aTp4MH834/rpJxc+/jjpH8VRo8J55plIK0dkXcWLJ1KrVtIsrl9/te5sposXszZrKTWlS8djY2MQGWnz0DSvFxERkQfHmTNnsjRR4uOPP6Zs2bL4+/szcOBAChYsSM+ePVOMq1mzJmXLlqVixYosXbqUVq1apXuN4ODgZItqpWXGjBm89NJLdOzYkcDAQEaNGkXlypX59ttvk40bPHgwLVq0oEyZMowYMYILFy5w5swZHBwccHNzw2Qy4e3tjbe3d7pJpm7dutGpUyf8/f158803iYiI4M8//8wwTrOvv/6abt260adPH8qUKcOgQYN44okn+PrrrzN9josXL1KlShUeffRRSpUqRaNGjWjVqlWmj79bYGAg58+fT3Vfhw4duHr1Knv37rVsW7p0KZ06dcrxVj95aiYTQL169QgPD2fhwoWEhobi5+fH22+/bSlbCwkJSfagxMTE8O2333Ljxg0cHBzw8fHhlVdesUzBs7Gx4dKlS0ycOJHbt2/j5uZGmTJleP/99ylVqpTlPK+++irfffcdY8eOxWQyUadOHfr27Zur9/6gOnUq6WUWGJh81S1f3wTeeus2I0bcTnV2k7f3v72bNLspd9y6ZeLNN7NeJpeWli2T92nq0iX/9mlatsyZt95KemyGDLnNSy/dsXJEeUOHDtHs2+fIypXO9O+fu32Z0pq15OmZQPfuUfTqFZFuUum/HBygZMkEzp2zIyjIjqJFH7A6TxEREblnhrMzl0+etNq1MzUuC2V1kJS86datG9euXeODDz7g+eefx9/fP8W4pUuX4uTkxIEDB5g6dSrjx4+/7zhu377NlStXqFWrVrLtNWvW5OjRo8m23V3GZu47FBISkuWqo7vP4+LigpubGyEhIZk+/tSpU/Tu3TvZtlq1avHdd99l+hzPPfccAwYM4PDhwzRu3JjWrVuneAwyyzCMNBNGXl5eNGrUiCVLllCnTh3OnTvHH3/8kSuLleW5JBNAmzZt0iyPGzNmTLLve/ToQY8ePdI8l4ODAyNGjMjwmq6urrz22mtZilMyx1wuV6ZM6ku7m2c3tWsXzdmztsyd68KCBS5cu2bL1KluTJvmSqNGMTzzTCQtW2plupz03nsFuXbt3srk0lKmTAIrV4YwdKgHv/3mzP/+V4i//nJgzJgw0pllm6esX+/Ia695YBgmnnsugjffzJ7H5kFgjZK5ixeT3if+u0Jc/fox9O4dQZs20Tg63tu5/f3jOXfOjuBgu4eumbuIiIikw2TKVMmaNfn7+2MymdLtl3Q3T09P/P398ff3Z8aMGbRo0YJHH32UcuXKJRtXqlQpChYsSGBgIDdu3ODFF19kyZIlaZ43ICAg0zFkhrmxNfy74FZiYtZ/57T/zwdJk8lkOY+NjU2K5FhGLXj+6+6V3szi45N/Bm7WrBl79+5l48aNbN++nR49evD888/z3nvvZelakJT0unvizH916dKFd999lw8//JClS5dSsWLFDFcFzA6qB5AcFR8PZ86kn2S6m3l20759V/nmm5s0ahSNYZjYutWJAQM8qV27KOPHu3HunG2G55KsWbfO8b7L5NLi5pbUp2nEiKQG+7NnF6B7dy+uX8/7b0G7djkweLAn8fEmOneO5KOPwtSr5y65WTJ37JgdAwcWok4dbz7/3I0rV2zx9EzgxRfvsH37VRYuvEHHjveeYALw9/+3L5OIiIhIflKoUCGaNGnCDz/8QGRkyrYOYWFhaR7r4+NDhw4dGDduXLrX6NOnD8ePH2fNmjVpjunUqRNBQUGsXbs2xT7DMAgPD8fNzY1ixYqxb9++ZPv379+fIsmVHgcHBxIS7r/qxcvLizt37iR73P7+++9kYwIDAy1Nx8327dtH2f/v1WVutXP16tU0z2G+Vrdu3Zg6dSpjxoxhzpw5WY53x44dHDt2jHbt2qU5pnXr1sTExLB582aWLVtG586ds3yde5H3P+FJvnbunC1xcSacnBIpUSLzP/zm2U3z5v3bu6lw4QTL7KZ69bzp3Vu9m7JL0mpyHkD2lMmlxtyn6Ycfblj6NLVpk7f7NB06ZE+fPp5ER5to2TKayZNDSeUPFA+9Dh2SmhWuXJkzSSZzcqlFC29Wr3a29FqaPv0m+/df5Z13wrNUFpcerTAnIiIi+dlHH31EYmIi7dq1Y/Xq1QQFBXHy5Em+++47nnzyyXSP7d+/P+vXr+fQoUNpjnF2dqZXr15MnDgxzbK4J598kieffJKXXnqJKVOmcOjQIS5cuMD69evp3r27ZaW1wYMHM336dJYvX86pU6f4+OOP+fvvv+nXr1+m77dkyZJERESwfft2bt68SVTUvbXlqF69Os7OzowfP54zZ86wdOlSFi1alGzMiy++yMKFC5k9ezZBQUHMmDGDNWvWMHjwYMtjU6NGDb788ktOnjzJ7t27+eSTT5Kd49NPP2Xt2rUEBwdz/PhxNmzYYElSpSU2NpZr165x+fJlDh8+zJQpU+jbty8tWrSga9euaR7n4uJCmzZt+PTTTzl58iSdOnW6p8cmq/RxSXKUuVQuICDhnj+cpzW7acuW5LObzp/XzIN7lRNlcmlp2TKGVauuExgYx5Urtjz1VGEWLLBu0+jUnDhhR+/enkRE2FCvXgxff31TpZppaNs26R9zc8lcdvlvcgmgffsoNmy4li2zllKjJJOIiIjkZ76+vvz222/Uq1ePsWPH0rx5c3r06MGOHTsynKVUrlw5GjduzGeffZbuuD59+nDy5ElWrlyZ6n6TycSXX37J6NGjWbt2LU899RQtWrRg0qRJtG7dmsaNGwPQr18/Bg4cyNixY2nRogWbN29m1qxZyVaWy0itWrV49tlnefHFF6latSrTp0/P9LF3K1SoEFOnTmXjxo00b96cZcuWMXz48GRj2rRpw/vvv8+MGTNo1qwZP//8M5MmTbL0g4akFezi4+Np06YNo0ePZuTIkcnOYW9vz7hx42jRogVdunTB1tY2w5g3b95M9erVefzxx+nduze7du3igw8+YNasWdjapv8ZuHPnzhw9epQ6derg4+OTxUfl3piMrHYHk3Rdv349y7WbD7Kvvy7ABx8UpEOHKL7++la2ndfcu2n+fBdCQpJ+sEwmg8aNk3o3tWih3k2ZtW6dIy+84IWNjcGyZSE89ljuvH5v3zbx2mserF2blDzo0yeCMWPC8sTzdu6cLZ07F+bKFVuqVYtlwYIbuLrqrTI9nTp5sW+fI++/H3bfDcCPHbNj8mQ3S2IJkpJLw4bdpkKFjMtu70dQkC0NGxbFycng5MnLmrkmIiLyEAsPD8fd3d3aYYjkqrRe9/b29hQpUiTD4/Xrs+Qo80ym/64sd7/unt00Y0by2U39+2t2U2bdXSY3aFBEriWYIKlP07ff/tun6Ycfkvo0Xb5s3belq1dt6NHDiytXbClfPo6fflKCKTPat08qmVu16t6bef3zjx2DBqWcubRx4zVmzLiV4wkmgFKlErC1NYiONln9tSgiIiIikt/oN2jJURmtLHe/HBySPtzOm3eTnTtT9m6qW9ebZ57xZM0a9W5Kzd1lcuZkT24y92maNSupT9PvvzvSoEFRxoxx59q13H97unnTRM+eXpw9a0fp0vHMnXsDT08lmDKjXbukkrl9+xyzXDJnTi41b+7NqlXJy+JyK7lkZm8PpUubm3+rZE5EREREJCuUZJIcdepUzsxkSo2fX+qzmzZvTprdVKdOUSZM0Owms7tXk5s0KXtXk8uqVq2S+jTVrBlLdLSJmTNdqVs3d5NNd+6YeO45L44ft6do0QTmz79BsWJZXxo1P4mOj862c93LKnMZJZcqVsy95NLd1JdJREREROTeKMkkOSY01MSNG0kJnYCA3PuwmNbspqtXbZkyRbObwLplcmkJDExg2bIQ5s69wWOP5W6yKToaXnjBk4MHHfDwSGTevBv4+mbPamV51bi94wicFcjGcxuz7ZyZLZnLq8klMyWZRERERETujZJMkmPMpXLFiiVQoIB1So7+O7upYcMYzW4CRo+2bplcWkwmaNw4huXLcy/ZFBcHL75YiF27HClQIJE5c25Qvrx1kxw5bdmpZUw7NA0Dgzn/zMm282ZUMpdacqldu7yTXDIzJ8WDgx+u9wURERERkfulJJPkmNwslcuIeXbT/Pk32LnzKi+//PDOblq3zpHFi/NGmVxacivZlJgIw4d7sG6dM46OBj/8cJNq1R7sF8Cxm8cYsX2E5futF7YSFR+VLedOq2QuveTSN9/kneSSmb+/ejKJiIhIksTEB7t9gsjdsuP1riST5JigoJxt+n2v/PwSePvtpNlNX3+d+uymTz55MGc35cUyufT8N9lUo0b2JZsMA959tyBLlrhgZ2cwY8ZN6tWLzeY7yFvCYsLov74/UfFRNPZpjI+rD9EJ0Wy/uD3brnF3yVx+Sy6Zmcvlzp61I+HBrpoUERGRdLi4uHD79m0lmuShkJiYyO3bt3Fxcbmv85gMw9DSSdno+vXrxD3oU2EyqV+/Qvz2mzNjx4bRr1+EtcNJ15kztsyd68KCBS6EhCQll0wmgyZNYnjmmUhatIjG7gGY1PDqqx4sXuxCYGAca9dez5OzmNJjGLBtmyOffebGgQMOADg5GTz7bAQvvXQHb+/M/wIwYYIbU6a4YTIZTJ0aSufO2TObJ69KNBLpu64v68+tp6RrSdZ0XsOkPyYx6+gsepTrwcTGE7PlOpcv21CzZrEU29u1i2LYsNt5NrF0t4QECAwsTmysid27r1pWmxMREZGHT3x8PJGRkdYOQyRXuLi4YJfGB197e3uKFCmS4TmUZMpmSjL9q0mTIpw8ac/cuTdo3DjG2uFkSmwsrF3rxJw5Bdi+3dGyvWjRBHr0iKRXr0hKlsyfHzjXrXPkhRe8sLExWLYsJM/PYkrP/SabvvqqAB9+WBCAjz8O5fnnH/xfHL44+AWf7P8ER1tHlnVYxiNFHmHbxW30/LUnXk5eHOx9EFub7Jm917mzF3v3Jv385Kfk0t0aNy7CqVP56/1LRERERCSnZDbJpHI5yRHx8XDmTN4sl0uPgwN06JDUu2nHjuS9m774wo3HH/fm2Wc9+e03J+Lzz23luzK5jJjL6FasSK2Mzpv330+7jG7OHBdLgumtt8IfigTT1gtb+XT/pwB8XP9jHinyCAB1i9fF3cGdG9E3OHDtQLZd79NPw3jttdt5viwuPf/2ZXrwymZFRERERHKKkkySI86dsyUuzoSTUyIlSuTPmT/+/qn3btq0yYl+/TypXTupd9OFC3n/Q6h5NbkyZeL43//yzmpy9yv1ZJMN33yTerJp+XIn3ngjKcH08su3GTLkjrVCzzXnb5/npU0vYWDQu0JvepTvYdlnb2NPs1LNAFh7dm22XTMwMJ6RI/Pf7KW7mfsymXvLiYiIiIhIxpRkkhxx+nTSB7OAgARs8vmrLLXZTV5eKWc3rV2bN2c33b2a3OTJoTg7Z3xMfnN3smnOnNSTTYsXO/Pqq4UwDBPPPBPBW2/dtnbYOS46PpqBGwYSGhNKtSLV+KDeBynGtPJtBWRvkulBYE4yaYU5EREREZHMy+cf/yWvMieZ8lOpXGaYZzft3580u6lBg39nN/Xt++/KdHlldlNoqIk33/QAYODA/F8mlxGTCZo0ST3Z9OqrhYiPN9GpUyQffxyGyWTtaHPeO7ve4a+Qv/B08uSbFt/gaOuYYkyzUs2wt7EnKCyIU6GnrBBl3qQkk4iIiIhI1inJJDniQU0ymZlnNy1YkDS76aWXkmY3XbmSt2Y3jR5dkKtXk8rkRox4cMrkMpJasgmgZctoPv88FNu8kQPMUXP+mcO84/OwMdkwvdl0fFx9Uh3n5uBGveL1AFh3dl1uhpinBQQk/eCeP2+bJ2coioiIiIjkRUoySY4wJ5kCAx/8T2f+/gmMGpU0u+mrr1Kf3fTpp7k/u2ndOkd++eXBLpPLyN3Jpl27rvL99zext7d2VDnvz+t/8s7OdwB4o+YbNPRpmO74Vn4qmfuv4sUTcXIyiI83cf78Q5CVFBERERHJBkoySY540GcypcbBAZ58Mml20/btyWc3ff557s5uetjK5DJiMoGvb/7vD5YZN6JuMGD9AGITY2nj24aXH305w2NalU5KMv1x9Q+uR17P6RDzBRsb8PNTyZyIiIiISFboN2fJdqGhJkJCkv7y/zAlme4WEJA0u+n112/z229OzJlTgB07HNm0yYlNm5woViyBHj0iqVEjNkd6A82f7/JQlsk97BISE3hp00tcirhEQMEAJjeZjCkTL7ASriV4pPAj/BXyFxvObaBnhZ65EG3e5+8fzz//2P9/kinG2uGIiIiIiOR5SjJJtjPPYipWLIECBQwrR2Nd5tlNTz4ZTVCQLfPmubBggYtldlNOsrExmDTp4SyTe1h9sv8TdlzagYudC9+2+BZ3B/dMH9vKtxV/hfzF2rNrlWT6f/82/1a5nIiIiIhIZijJJNnu1KmHr1QuM8yzm0aMuM3atU4sWuRCSEjO1G+ZTNC1axQ1az7cZXIPkzXBa5h2aBoAnzX6jPKe5bN0fGvf1nz2x2dsv7idyLhIXOxdciLMfMXfPwGAoCD9UykiIiIikhn6zVmynfkD2cPQ9PteODr+O7tJJDucCj3F0K1DARhQZQAdy3TM8jkqelaklGspzt85z7aL22jj1yabo8x//p3JpH8qRUREREQy4yFogyu57WFs+i1iLRFxEQxYP4A7cXd4vNjjjKoz6p7OYzKZtMrcf5iTTBcu2BIba+VgRERERETyASWZJNupXE4kdxiGwYhtIzgReoJiLsX4qvlX2NvY3/P5Wvu2BmDDuQ0kJCZkV5j5VtGiibi4JJKYaOLcOc1mEhERERHJiJJMkq3i4+HMGZXLieSGmUdmsiJoBXYmO75u8TXeLt73db46xerg4ejBzeib7L+6P5uizL9MJvDzM/dlUvNvEREREZGMKMkk2er8eVvi4kw4OSVSooRmQojklD2X9/Dh7x8CMKbuGGoVrXXf57SzsaNZqWaASubM1JdJRERERCTzlGSSbGUulfP3T8BGry6RHHEl4gqDNw4mwUigS2AX+lTqk23nNpfMrT27FsMwsu28+ZWSTCIiIiIimac0gGQrc9NvlcqJ5IzYhFgGbRzE9ajrVPSsyCcNP8FkMmXb+ZuUbIKDjQNnws9wMvRktp03vwoIUJJJRERERCSzlGSSbBUUpKbfIjlp7J6x7L+6H3cHd2a2mImznXO2nt/VwZUGPg0AlcwBBAQklf0GB6snk4iIiIhIRpRkkmylleVEcs7ik4uZdXQWAFOaTMG/oH+OXKdl6ZaAkkzwb7ncpUu2REdbORgRERERkTxOSSbJViqXE8kZf9/4m5HbRwIwtPpQWvq2zLFrtfJtBcDBawe5Gnk1x66TH3h5JeLmlohhmDh7ViVzIiIiIiLpUZJJsk1oqImQkKSSEnMfExG5f6ExoQxYP4DohGialmzK8BrDc/R6xQoUo1qRagCsP7s+R6+V15lMav4tIiIiIpJZSjJJtjHPYipWLAFXV61KJZIdEo1EXtvyGmdvn6WUaymmNp2KrU3O9wcyz2ZSydy/SSZzzzkREREREUmdkkySbcxJJvVjEsk+Xxz8gg3nNuBk68S3Lb+lkFOhXLlua9/WAOy8tJOIuIhcuWZe5e+v5t8iIiIiIpmhJJNkGyWZRLLX5vObmfjHRAA+bvAxVQpXybVrly9UHl83X2ISYthyYUuuXTcvUrmciIiIiEjmKMkk2UZNv0Wyz7nwcwzZPAQDg2cqPEP3ct1z9fomk+nfkrkzD3fJnJJMIiIiIiKZoySTZBvNZBLJHlHxUQzYMIDQmFCqF6nO2HpjrRJHa7+kkrmN5zcSn/jw/lybk0xXrtgSGWmycjQiIiIiInmXkkySLeLj//0rv5JMIvfOMAze3vk2R24cwcvJixktZuBo62iVWGoVrYWHowehMaHsvbLXKjHkBZ6eBh4eiYD6MomIiIiIpEdJJskW58/bEhdnwsnJwMcnwdrhiORbP//zMwtPLMTGZMP0ZtPxcfWxWix2Nna0KN0C0CpzKpkTEREREcmYkkySLcylcv7+8djoVSVyTw5cO8C7u94F4K1ab9HAp4GVI/p3lbl1Z9dhGIaVo7EeJZlERERERDKmdIBki1OnVConcj9CokIYuGEgcYlxtPVry4uPvGjtkABoXLIxjraOnLt9jn9u/WPtcKwmIEBJJhERERGRjCjJJNkiKEgry4ncq/jEeF7c+CKXIy5TpmAZJjWehMmUNxpMF7AvQIMSSTOqHuZV5vz9k8qA1ZNJRERERCRtSjJJttBMJpF7N2HfBHZd3oWLnQvftvwWNwc3a4eUjHmVuXVn11k5EutRuZyIiIiISMaUZJJsYe7JpCSTSNasDl7N9L+mAzCp8STKFSpn5YhSalm6JSZMHAo5xOWIy9YOxyrMSabr1225fTtvzDITEREREclrlGSS+xYaaiIkJKmEREkmkcw7FXqKYVuHATCo6iA6BHSwckSp83bxprp3deDhnc3k7m7g5WUumdNsJhERERGR1CjJJPfNPIupWLEEXF0f3tWnRLLiTuwd+q/vT0RcBHWL1+Xt2m9bO6R03b3K3MNKfZlERERERNKnJJPcN5XKiWSNYRgM3zack6EnKVagGF81+wo7m7w9O8acZNp5aSe3Y29bORrrMJfMmRc6EBERERGR5JRkkvumJJNI1sw4PIPVwauxt7FnRvMZFHEpYu2QMhToEYi/uz9xiXFsubDF2uFYhZp/i4iIiIikT0kmuW9KMolk3q5Lu/h478cAjKk7hppFa1o5oswxmUwP/SpzSjKJiIiIiKRPSSa5b+YkU2Cgkkwi6bl05xKDNw4mwUiga9muPF/xeWuHlCXmkrmN5zYSlxhn5WiyT1R8FMtPL6ffun70/LUnd2LvpDouIMCcZFJPJhERERGR1OjPsXJf4uPhzBnNZBLJSExCDIM2DuJG9A0qeVZifIPxmEwma4eVJY95P4aXkxc3om/w++XfaeDTwNoh3bPYhFi2XtjK8tPLWXt2LZHxkZZ9m85v4skyT6Y4xs8vqfH3rVu2hIaa8PDQQgciIiIiIndTkknuy/nztsTGmnByMvDxSbB2OCJ51vt73ufAtQMUdCjIzJYzcbZztnZIWWZrY0uL0i1YcGIB686uy3dJpoTEBPZc2cPy08tZHbya0JhQy77SbqWxNdkSHB7MudvnUj3e1dWgaNEErl61JTjYjurVH5zZXCIiIiIi2UHlcnJfzKVy/v7x2OjVJJKqRScWMfvobACmNp2Kn7ufdQO6D+aSubVn12IYeX8mj2EYHLx2kNG7R1NrXi26re7GnH/mEBoTirezN/0q92PFkyvY1X0XnQI7AaSZZAL1ZRIRERERSY9+S5b7cuqUSuVE0nPkxhHe3PEmAMNrDKd56eZWjuj+NCrZCCdbJy7cucDRm0ep7FXZ2iGl6vjN4yw7vYwVQSs4E37Gsr2gQ0Ha+relY5mO1CteD1ubf/sr+br5AiQb/1/+/vHs2eOoJJOIiIiISCr0W7Lcl6AgJZlE0nIr+hYD1g8gOiGaZqWaMazGMGuHdN+c7ZxpVLIR686uY93ZdXkqyXQu/BzLg5az/PRyjt08ZtnubOdMK99WdCrTicYlG+No65jq8b7uvpbzpMXfP6ksWM2/RURERERSUpJJ7otWlhNJXaKRyKtbXuXc7XOUdivNlCZTsDE9GDWlrX1bs+7sOtaeXWv1xNm1yGusDFrJstPLOHDtgGW7vY09TUo2oVOZTrTybYWLvUuG5zInmS5GXCQ2IRYHW4cUY1QuJyIiIiKSNv2WLPdF5XIiqfv8wOdsOr8JJ1snZracSSGnQtYOKdu0KN0CEyYOhxzm4p2L+Lj65Or1Q2NCWRO8hmWnl7Hr8i4SjUQATJioV6Iencp04gm/J7L8mHs7e+Nk60R0QjQX71zEv6B/ijF3J5kMA/LZAoEiIiIiIjlKSSa5Z2FhJkJCkkpGAgKUZBIx23huI5MOTAJgfIPxVPGqYuWIsldh58LULFqTfVf3sf7sevpU7pMr1zUMg0//+JTph6YTl/jvym7VvavTqUwnOgR0oKhL0Xs+v8lkwtfdl+O3jnM2/GyqSSY/v6RyubAwG27etMHLK/GeryciIiIi8qBRkknumblUrlixBNzc8v4qUyK54Wz4WV7Z/AoGBs9Xep6nyz1t7ZByRGvf1uy7uo+1Z9fmSpIp0Ujk3V3v8sPRHwCoUKgCHct0pGOZjpYyt+xQ2q10UpLp9tlU9zs7GxQvnsDly7YEBdkqySQiIiIicpcHo0GIWIW5VE6zmESSRMVH0X99f8Jiw6jhXYMxj4+xdkg5ppVvKwB2X95NeGx4jl4rITGBkdtH8sPRHzBhYnyD8WzsupFXq7+arQkmuKv59+30mn+rL5OIiIiISGqUZJJ7pqbfIv8yDIM3tr/B0ZtHKexcmBnNZ6TaOPpBUcajDIEegcQlxrH5/OYcu058YjxDtw5l3vF52JhsmNx4Ms9WfDbHrufrlpRkOhue+kwmUJJJRERERCQtSjLJPTMnmdT0WwRmH5vN4lOLsTXZ8lWzryjhWsLaIeW41r6tAVh7dm2OnD82IZaXNr3EklNLsDXZMq3ptBwvPzTPZEovyWSevakkk4iIiIhIckoyyT1Tkkkkyf6r+xmzewwAb9d+m3ol6lk3oFxiLpnbdG4TsQmx2Xru6PhoBm4YyOrg1djb2PNNi2/oWKZjtl4jNZYk0+2zGEbqveb8/ZOafwcH2+Z4PCIiIiIi+YmSTHJP4uPhzBmVyz1srkde5+VNL/PD3z+k+QH8YXM98jqDNgwiLjGOdv7tGFR1kLVDyjXVi1SnsHNhbsfdZs/lPdl23qj4KPqu68v6c+txsnViVqtZtPFrk23nT09J15KYMBERF8HN6Jupjrl7JpN+DERERERE/qUkk9yT8+dtiY014eRk4OOTYO1wJBckGom8tuU1lp1exqhdoxiyeQhR8VHWDsuq4hPjeXHTi1yJvEJZj7JMajQJk8lk7bByja2NLS1LtwSyr2QuIi6CZ397lq0Xt+Js58zs1rNpWqpptpw7M5zsnChWoBgAZ8LPpDqmdOl4bGwMIiJsuH5d/4yKiIiIiJjpt2O5J+ZSOX//eGz0KnoofP/392y9uBVHW0fsTHYsO72MTis6cfHORWuHZjUf7/2Y3Zd3U8C+AN+2/BZXB1drh5TrzCVz686uu+/ZbeGx4fT8tSe7L+/G1d6VeU/Mo4FPg+wIM0v83P2AtFeYc3TEklxXXyYRERERkX8pPSD3RP2YHi7Hbh7j470fA/BenfeY324+nk6eHLlxhCeWPpGtpVL5xcqglcw4PAOAyY0nE+gRaOWIrKOhT0Oc7Zy5FHGJIzeO3PN5bkXfosfqHvxx7Q8KOhRkftv51CpWKxsjzbzSbqWBtGcywd0rzKkvk4iIiIiImZJMck+UZHp4RMdHM2TTEGISYmhWqhnPV3qeusXrsqbTGip7VeZG9A26r+7OD0cfnj5NJ2+dZPjW4QC8+MiLtPNvZ+WIrMfZzpnGPo2Bey+ZC4kK4enVT3Mo5BCeTp4sbL+Q6t7VszPMLDE3/05rJhPc3fxbM5lERERERMyUZJJ7oiTTw2PcvnH8c+sfvJy8kvUcKulWkuVPLqdjmY7EG/GM2jmKkdtHEpMQY+WIc9bt2Nv0W9+PyPhI6hWvx5u13rR2SFbXyi+pZG7tmawnma5EXKHrqq4cu3kMb2dvfmn3C1W8qmR3iFni6/b/K8yFn01zjHkmU1CQkkwiIiIiImZKMsk9MSeZtLLcg23L+S18e+RbACY1nkQRlyLJ9jvbOfNl0y8ZVXsUJkzMPT6Xbqu7cS3ymhWizXmGYTB863BOh52meIHifNX8K+xslGRoWbolNiYbjt48yvnb5zN93MU7F3lq1VOcDD1J8QLF+aX9L5T3LJ+DkWaOeSbT2dsZJ5k0k0lERERE5F9KMkmWhYWZuH49qQ+JeSnvjJy4dYJtF7YRlxiXk6FJNroRdYNhW4cB8Hyl52lRukWq40wmEy89+hI/tfkJdwd39l/dzxPLnuDP63/mYrS546u/vuLXM79ib2PPNy2+obBzYWuHlCd4OnlSq2hS/6R1Z9dl6piz4WfpsrILZ8LPUMq1FEvaL6GMR5mcDDPTzEmmKxFX0lxB8e6eTImJuRaaiIiIiEiepiSTZJl5FlOxYgm4uWXcgycuMY4uK7vQc01PHpvzGGN2j+HYzWM5HabcB8MweH3761yLukZZj7K8W+fdDI9pWqopqzutpqxHWa5EXKHLyi4sOrEoF6LNHTsu7mDcvnEAjK07lhreNawcUd5iXmUuM32ZToWeosuqLly4cwF/d38Wd1hMaffSOR1iphVyLISbvRsAF25fSHVM6dIJ2NoaREfbcOWK/ikVEREREQElmeQenDqVlGTK7Cymc+HnuBVzC4Ab0TeYeWQmLRa3oM3SNnx/5HtuRt/MsVjl3sz5Zw5rz67F3saeac2m4WznnKnjAgoGsLLjSlr5tiImIYahW4cyevdo4hPzd1nlxTsXeXHTiyQaiXQr141nKz5r7ZDynNa+rQHYc3kPoTGhaY775+Y/dF3VlSsRVyjnUY7FHRbj4+qTS1FmjslksiS90lphzt4eSpVS828RERERkbspySRZltWm30FhQQBUKFSBWa1m0davLfY29hwOOcy7u9+lxpwaDNgwgPVn1+f7ZMSD4FToKUbvHg3Am7XezHITZjcHN75r+R3DaiSV2n175Ft6r+mdb5OJMQkxDNowiJvRN6niVYWP639saX4u//Iv6E85j3IkGAlsOr8p1TFHQo7QdVVXrkddp5JnJX5p/wtFXYrmcqSZk7kV5tSXSURERETkbkoySZaZV1PKbNPv02GnAShbqCytfFsxs+VMDvQ+wNi6Y6niVYW4xDh+Df6VPuv6UHNuTT74/QOO3zyeY/FL2mITYnll8ytEJ0RTv0R9BlYdeE/nsTHZMOKxEcxsMRMXOxd2XNpBu2XtOHrjaDZHnPPe2/UeB68fxMPRg5ktZmZ6VtfDKL1V5g5cO0C31d24FXOLakWqsbDdQrycvXI7xEzLygpzSjKJiIiIiCRRkkmyzFwul9WZTAEFAyzbPJ086VelH2u7rGVdl3UMqDIALycvrkdd5+u/vqbZ4ma0XdqWH/7+gVvRt7L/JiRVE/+YyF8hf+Hh6MHnjT/HxnR/bxFt/duyouMKfN18OXf7HE+ueJJVQauyKdqct+D4An7+52dMmJjadGqe6huUF5lL5jZf2ExMQoxl+94re+n5a0/CYsOoVbQW89vOp5BTIWuFmSmZWWHOXDIcHGybKzGJiIiIiOR1SjJJliQkwJkz959kultlr8qMqTuG/b32833L72nj2wY7kx2HQg4xatcoasypwaANg9h4bqPK6XLQ7su7+fLQlwB80vATSriWyJbzVvSsyOpOq2no05Co+CgGbRzEhH0TSDTy9pJch0MO89bOtwD432P/o1mpZlaOKO+rVqQaRV2KEhEXwa5LuwDYfnE7vdb04k7cHeoVr8ecJ+bg5uBm5UgzlrmZTOrJJCIiIiJyNyWZJEvOn7clNtaEk5OBj09Cpo4JDgsGoEzB9Jcnd7B1oLVfa75r9R1/9P6DMY+PoaJnRWITY1kVvIrn1j5H7Xm1+ej3jzhx68R934v8KzQmlFc3v4qBQY9yPWjn3y5bz1/IqRA/t/nZUn435c8pvLDuBcJjw7P1OtnlZvRNBqwfQExCDM1LNee16q9ZO6R8wcZkQ4vSLYCkVeY2nd/E82ufJyo+iqYlm/Jjmx8pYF/AylFmjnnW2vnb59NMiJrL5c6etSMxb+dMRURERERyhZJMkiXmUjl//3hsM1EhEhEXwZXIK0nHFPTP9HUKOxdmQNUBbHhqA2u7rKVf5X4UcizE1cirTP9rOk1/aUr7Ze2ZfXR2uitZScYMw+CtHW9xKeISfu5+jK03NkeuY2djx+jHR/NFky9wtHVkw7kNdFjegdOhp3PkevcqITGBVza/wvk75/F182VK0yn3XTb4MDGXzC0/vZy+6/oSkxBDa9+k5HF+6mfl4+qDrcmW6IRorkZeTX2MTwL29gYxMSYuXVLJnIiIiIhInpzj/9tvv7Fy5UpCQ0Px9fWlb9++BAYGpjr2999/Z+nSpVy5coWEhASKFStGhw4daNSoEQDx8fHMnz+fgwcPcu3aNVxcXKhatSq9evXC09PTcp6XX36Z69evJzt3r1696NSpU47dZ35kXlnO3IskI+ZZTF5OXng4etzTNat4VaFKvSq8U+cdNp7byMKTC9l4biMHrx/k4PWDvL/nfVr7tqZbuW408mmErY0+7GXF4lOLWRG0AluTLVObTs3xmSZdy3alrEdZ+q3vx6nQU7Rf3p5pTafRvHTzHL1uZk06MIktF7bgZOvEzJYz7/l1+7CqX6I+LnYulllqHQI6MLXpVOxt7K0cWdbY29jj4+rDudvnOBd+juIFiqcYY2cHpUvHc/q0PUFBtpQsmbnZnSIiIiIiD6o8l2TatWsXP/74IwMGDKBs2bKsXr2ajz76iM8//5yCBQumGO/q6kqXLl0oUaIEdnZ2HDhwgOnTp+Pu7k61atWIjY0lODiYp556Cj8/P+7cucMPP/zAJ598wvjx45Odq1u3brRo0cLyvZOTU47fb35jTjJldWW5tPoxZYWDrQNP+D/BE/5PcD3yOktOLWHhiYX8c+sfVgStYEXQCoq5FKNr2a48Xe5pAj1ST0zKv86Gn2XUzlEADK8xnBreNXLluo8WeZQ1ndYwYMMA9l3dx/Nrn+fNWm/y8qMvYzKZciWG1Kw/u57PD34OJPWlquxV2Wqx5FdOdk608WvDklNLeCrwKSY1noSdTZ77pyZTfN2TGtafuX2GOsXrpDrG3z+B06ftCQ62o1Gj2FyOUEREREQkb8lzNSCrVq2iefPmNG3alJIlSzJgwAAcHBzYvHlzquMrV65M7dq1KVmyJMWKFaNt27b4+vryzz//AODi4sK7775LvXr1KFGiBOXKlaNv374EBQUREhKS7FzOzs54eHhYvpRkSsmcZMqupt/3qohLEQY9MogNT23gt86/8UKlF/Bw9OBK5BWmHZpG40WN6bC8Az8f+5mwmLBsvfaDIj4xnlc2v8KduDvUKlqLV6q9kqvXL+JShIXtFvJMhWcwMBi3bxwvbXqJyLjIXI3DLDgsmFe3vArAC5Ve4KmyT1kljgfBx/U/Zkn7JXze5PN8m2ACKO2W1JfpXPi5NMeY+zIFBeXf+xQRERERyS55KskUHx9PUFAQVatWtWyzsbGhatWqnDiRcaNnwzA4fPgwly5dolKlSmmOi4yMxGQy4eLikmz7smXL6Nu3LyNHjmTFihUkJKRd+hAXF0dkZKTlKyoqKhN3mP/llSSTmclkomrhqnxY/0MO9D7ANy2+oUXpFtiabDlw7QBv7HiDGnNq8PKml9l6YSsJiSpnMZv651T+uPYHbvZuTG061Splhg62DkxoOIHxDcZjZ7JjRdAK6i2ox/RD07kdezvX4oiKj2LAhgGEx4bzmPdjvPf4e7l27QeRm4MbdYrXyfe9rPzc/QA4dzvjJJNWmBMRERERyWPlcuHh4SQmJuLh4ZFsu4eHB5cuXUrzuMjISAYNGkR8fDw2Njb069ePRx55JNWxsbGxzJkzh/r16ydLMj3xxBP4+/vj6urK8ePHmTdvHrdu3eL5559P9TxLly7ll19+sXzv7+/PhAkTsnC3+U9YmInr15MSEXklyXQ3R1tH2vm3o51/O65GXmXpqaUsOL6AE6EnWHZ6GctOL6N4geJJ5XRln6aMR/qr3T3I/rj6B5MPTAbgo/ofUcqtlFXjebbis5QvVJ5XN7/K+Tvn+WjvR0z7cxp9q/Slb+W+eDp5ZnySe2QYBiO3j+TYzWMUdi7MjBYzcLB1yLHrSf5hnsl0JvxMmmOUZBIRERER+dcD8Vuxk5MTn376KdHR0Rw+fJgff/yRokWLUrly8n4q8fHxTJ6c9MG6f//+yfa1b9/e8v++vr7Y2dkxc+ZMevXqhb19yoa1nTt3TnaMNfvI5BbzLKaiRRNwczMyHG8YhiXJVKZg7iZ0iroUZfAjgxlUdRCHQg6x8MRClp1axuWIy0z9cypT/5xKzaI16VauGx0COuDu4J6r8VnTndg7vLL5FRKMBDqV6USXwC7WDgmA2sVqs737dpaeWsq0P6dxOuw0kw9MZsZfM3iu0nMMrDqQoi5Fs/26Pxz9gSWnlmBrsuXr5l+n2uBZHk6ZmckUEJA0O/LcOVvi45OagYuIiIiIPKzyVC2Du7s7NjY2hIaGJtseGhqaYnbT3WxsbChWrBh+fn506NCBxx9/nGXLliUbY04whYSE8M4776QolfuvsmXLkpCQkGLFOTN7e3tcXFwsX87O+Wdp7nuV1VK5G9E3CI8Nx4QJX3ffnAwtTSaTiWpFqvFx/Y850PsAXzf/mmalmmFjsmH/1f2M3D6S6j9X55XNr7Dt4jYSjUSrxJmb3t39Lmdvn8XH1YeP63+cpxKk9jb2dCvXjc1dN/N186+p7FWZyPhIvv7ra+rOr8vbO9/m/O3z2Xa9fVf2MWb3GABG1R5F3eJ1s+3ckv+Vdk+ayRQSFcKd2DupjilRIgFHR4P4eBMXLmhlSxERERF5uOWpJJOdnR0BAQEcOXLEsi0xMZEjR45Qrly5TJ8nMTGRuLg4y/fmBNOVK1d49913cXNzy/AcZ86cwWQy4e7+8MxwycipU/fWj6mka0mc7KzfRN3JzokOAR34qc1P7Ou5j1G1RxHoEUh0QjRLTi2h5689qTOvDp/s/4TgsGBrh5sjVgatZOGJhZgwMaXJFAo6plyxMS+wtbGlQ0AH1nZey4+tf6Rm0ZrEJMQw++hsGixowNAtQzkVeuq+rnEt8hqDNg4i3oinQ0AHBlYdmE3Ry4PC3cGdQo6FgLRnM9nYgK+vSuZERERERCCPJZkgqWxt48aNbNmyhQsXLvDtt98SExNDkyZNAJg2bRpz5861jF+6dCl//fUXV69e5cKFC6xcuZLt27fTsGFDICnBNGnSJIKCgnjllVdITEwkNDSU0NBQ4uOTPhicOHGC1atXc+bMGa5evcr27duZPXs2DRs2xNXVNdcfg7zKvHpSXuzHlFXFChTjpUdfYkvXLazsuJJnKz6Lu4M7lyIu8cXBL2iwsAGdV3Rm/vH5ac5gyG8u3rnIG9vfAGBItSE8XvxxK0eUMZPJRPPSzVnWYRmL2i2ioU9D4o14Fp1cRJNFTRi0YRBHbhzJ+ET/EZcYx+CNg7kaeZVyHuWY2GhinprRJXmHeRbm2fCzaY4JCFCSSUREREQE8mBPpnr16hEeHs7ChQsJDQ3Fz8+Pt99+21IuFxISkuzDYExMDN9++y03btzAwcEBHx8fXnnlFerVqwfAzZs32b9/PwAjR45Mdq3Ro0dTuXJl7Ozs2LVrF4sWLSIuLg5vb2/atWuXrOeS/FsuFxiYuSTT6dDTQN5MMpmZTCZqeNeghncNxjw+hrVn17LoxCK2XtzK3qt72Xt1L+/seoe2fm3pVq4b9UrUy5crZiUaiQzdMpSw2DAeLfwo/3vsf9YOKUtMJhP1StSjXol6HLx2kCl/TmHd2XWsCl7FquBVNCvVjFerv0qtorUydb6Pfv+I36/8jqu9KzNbzqSAfYEcvgPJr3zdffnz+p+cvZ12ksnfP6kvU3CwyuVERERE5OFmMgwj4w7OkmnXr19PVqr3oEhIgMDA4sTGmti9+yqlSydkeEy/df347exvfFD3A/pW6ZsLUWafyxGXWXxyMQtOLLDMyIKk0r+nyz3N02WftlqfqXvx1aGv+HDvhzjbObO289oHYmW9YzePMe3PaawIWmHppVW3eF1erf4qDUs0THNm0vLTy3lp00sAfNviW57wfyLXYpb8Z/y+8Uz9cyrPVXyOcQ3GpTpmzhwXRo70oGnTaH7++WYuRygiIiIikvPs7e0pUqRIhuPy35QMsYrz522JjTXh6Gjg45NxggnydrlcRooXKM6QakPY9vQ2lj+5nN4VeuNm78aFOxeYfGAy9RbU46mVT7HgxAIi4iKsHW66DoccZsL+CQCMrTv2gUgwAVT0rMiXzb5k69Nb6Vm+J/Y29uy+vJuev/akw/IOrD2zNkUj9+M3jzNi2wgAhjw6RAkmyVBmVpjz91e5nIiIiIgIKMkkmWQulQsIiMc2ExUhCYkJnAk/A5Cvkxomk4maRWvyScNPOPjMQb5s+iWNfBphwsSeK3sYvnU41X6uxtAtQ9l1aVeeW50uKj6Klze9TFxiHG1829CzfE9rh5TtAgoG8Fmjz9jZfSd9K/fFydaJg9cP0nd9X1oubsmyU8tISEwgPDac/hv6ExkfSYMSDXi95uvWDl3ygdJuSSvMmd/PUmNOMp0/b8sDOJFVRERERCTTVC6XzR7UcrkZMwowdmxB2rWL4ptvbmU4/lz4OeouqIujrSMn+5zE1ubB6lVy8c5FFp9czMITCwkO/3clutJupXm67NN0LdvVsvy5Nb214y1+PPYjRV2KsuGpDXg6eVo7pBwXEhXCzMMz+eHoD9yJS2ra7ufuh7ezN3uv7qVEgRL81vk3vJy9rByp5AcX71yk9rza2JnsCOoblOp7mWFA2bLFiIqyYdu2q5Qpk7nZniIiIiIi+YXK5SRbmWcyZXVlOT93vwcuwQTg4+rDq9VfZXu37SzrsIxe5Xvhau/KudvnmHhgInUX1OXpVU+z6MQiIuMirRLjurPr+PHYjwB83vjzhyLBBFDYuTBv1X6L33v+zojHRuDh6MGZ8DPsvboXBxsHvmnxjRJMkmnFXIrhYONAvBHPpYhLqY4xmcDPLymxZF6FU0RERETkYaQkk2RKVleWy8/9mLLCZDJRq1gtPm30KX8+8ydTm06lQYkGmDCx6/Iuhm4dSrU51Ri+dTh7Lu8htyYOXou8xv+2Ja0gN6DKABqVbJQr181LPBw9GFZjGHt77uW9Ou9RvUh1Pm/yOdW9q1s7NMlHbG1sKeVWCoCz4emtMKe+TCIiIiIi+m1YMuVeZzI96EmmuznbOdMlsAtdArtw4fYFfjn5C4tOLuJM+BkWnFjAghML8HXztaxOV9KtZJbOn2gkEpMQQ1R8FJFxkUTFRyX7ioz/d9uy08u4GX2Tip4VebPWmzl0x/lDAfsCDHpkEIMeGWTtUCSf8nX35XTYac7ePksDGqQ6JiBASSYREREREf02LBkKCzNx/XpSyVtmk0ynw04DD1eS6W4l3UoytMZQXqv+Gnuv7GXhiYWsDF7J2dtn+eyPz/jsj8+oX6I+FTwrEB0fnTxZFBdJVEJUim3RCdFZisHR1pEvm36Jk51TDt2lyMPB180XSOo1lxbNZBIRERERUZJJMsE8i6lo0QTc3DJX7vUwzmRKjclkok7xOtQpXocP6n3A6uDVLDyxkF2Xd7Hz0k52Xtp5T+d1tHXE2c451S8XOxdc7FzoWaEn5T3LZ/MdiTx8zE38019hLqknU3Dwg9eDTkREREQks5RkkgyZk0zmcpCMRMVHcfHORQDKFCyTY3HlNy72LkmlcuWe5vzt86w4vYLw2HCc7JySJYiSJYzsXXC2Tf69k63TA9lMXSSv8nP3A+Dc7YxnMl28aEt0NDhpAqGIiIiIPISUZJIMZbXp99nwsxgYFHQo+NCsaJZVpdxK8XK1l60dhohkQmm3pJlM6TX+LlIkEVfXRO7cseHcOTvKlcvc+6WIiIiIyINEq8tJhu6n6bfJZMqxuEREcoOve1JPprDYMEJjQlMdYzKpL5OIiIiIiJJMkqF7TTL5F/TPsZhERHKLs50z3s7eQPqzmdSXSURy2qlTdowcWZApU1zZssWRmzf1q7yIiOQt+nOrpCsh4d+/yme2XE5Nv0XkQVPavTTXoq5xNvwsjxZ5NNUx5plMQUH6p1VEcsa4cW789ptzsm0lS8bzyCNxVK0axyOPJH15eiZaKUIREXnY6TdhSdf587bExppwdDTw8UnI1DGnw04DSjKJyIPD182X/Vf3Z6r5t8rlRCQnREXB1q2OALRoEc3p03YEB9tx4ULS16+//pt8UuJJRESsRb8JS7rMpXL+/vHYZrICxDyTSSvLiciDwtyXKf1yOSWZRCTn7NzpSFSUDSVKxPPDDzcxmSAszMSRI/YcPmzPX3/Zc+iQA2fOZJx4evTROKpWjcXT07DiHYmIyINIvwlLuk6dylo/plvRt7gZfRNQTyYReXBYkky3004yBQQkzfa8fNmWqCgTzs768JYRw0hqmi4iGVu/3gmAli1jLD83BQsa1K8fS/36sZZxSjyJiNwb/V6SPZRkknRltel3cHgwAMUKFKOAfYEci0tEJDf5umU8k6lQoUQKFkwkLMyG4GBbKlXK3Pvmw8YwYOlSZ8aNc8PTM5EVK0JwdLR2VCJ5m2HAhg3mJFN0umMzSjwdOuTAX3/ZZ5h4Mn8p8SQiD4NVq5x45ZVCfP75LTp2TP99VtKnJJOky9zANqsrywW4qx+TiDw4zDOZLkVcIjYhFgdbhxRjTKakkrk//3QgONhOSaZU/POPHaNGFWTPnqSs0qVLsGiRC888E2nlyETytsOH7blyxZYCBRKpVy8my8cnTzxFAP8mnv76y56//lLiSUQeXomJ8MknbsTGmli40EVJpvukJJOky1wup5XlRORhVsS5CM52zkT9H3v3Hd5U4TVw/HuTtE33gA5my96jIKNM2UtlqIACDpYgCiqCP/fEiQMFFAVFEAEBFZC99x6y9y500T3TJPf9I28KFQodaZO25/M8PApJ7j2FNuPcM4xpXEu+luNz3O1JJnFLUpLCl1968vPP7phMCnq9mRYtDGzZomf6dA8GDkxFJ39lQuRo7VpLFVP79hk2q/yTxJMQQlhs2eLC+fNOAOzb54zRiLwvKQD5qxM5SkxUiI62TPvOcyWTJJmEECWIoigEewZzKu4UVxKv3CPJZJnLdPFiLjcllHCqCn//7coHH3gRFWX5O+nZM413303Ez89M8+YBXL6sY/lyV/r2TbNztEI4rnXrLJml+7XKFVROiaejR60znu6deKpUyZhto50knoQQxcHMmbfGvKSkaDh2zInGjTPtGFHxJkkmkSPrPKaAABOenrl7g3A+/jwgSSYhRMlT2asyp+JOcSnpUo73kQ1zt5w6peOtt7zZtcvy4TgkxMhHHyXQocOtVp/hw1P44gsvpk71oHfvNDQae0VbsqWkKKxcqcfX10y7dhk439ntKRxYeLiGY8ecURSVTp3y3ipXUN7eKm3aGGjT5v6Jp6tXLb/ulnhq2tTAE0+k4u0tSSchhOM4d07H5s16FEWlbl0jx487sXu3sySZCkDeBYsc5XWznFk1Zw3+liSTEKKksQ7/vpJ4Jcf7VK0qSaakJIWvvvJk1qxbrXHjxiXz3HPJd7T5PPNMCt9/78GpU06sX+9C165F/wG6JDMaYcECN7780jOrkszb20zXruk8/HAabdtKwqk4sG6Ve+ABA2XKmO0cjUV+E0/ffefJmDHJPPtsMq6u9z6HEEIUhVmzLFVMXbum07y5gePHLbMjR41KsXNkxVfpfRcs7iuvm+UiUiJIM6ahU3RU9qpcmKEJIUSRsw7/vteGOWslU1SUluRkBQ+P0nPF3toa9+GHXkRGWhIaPXqk8d57iVSsaLrrY3x8VJ5+OoVp0zz59lvPbKvZRf5ZNpG5MGmSF2fPWmZMVKpkJCNDISpKy6JFbixa5CYJp2Li1lY5x07C3i/xtGiRG6dPOzFpkhezZrkzfnwS/fvLPDYhhP3ExyssWmTJeA8bloK7u+V92969zpjNSIV1Pslfm8iRNcmU16Hflb0q46RxKrS4hBDCHrKSTEk5J5m8vVX8/CwJlUuXSs9cptOndTz+eBleeMGXyEgtISFGfvvtJjNnxuWYYLIaPjwFFxeVQ4ec2blTshwF9e+/Tjz+eBmeeaYMZ8864eNj5v33E9i6NYr9+yNZsiSGZ59NJiDAREKChkWL3HjqqTI0bhzESy/5sGGDCwaDvb8KYZWcrLBjh6UEsGvX4rftyJp4Gj06hXXrovn66zgqVDASEaFlwgQfOnb0Z8UKPWrpyccLIRzI/PlupKVpqFMnk1atDNSvn4m7u5mEBA0nT0oGPL8kySRylNdKJhn6LYQoySp7Wio0LydeRr3HJyLr8O8LF0r+m5OkJIX33/eiSxd/du1yQa83M3FiIhs3RmWbvXQvAQFmBg5MBeC77zwLM9wS7epVLWPG+NCzp+XfwsVF5fnnk9i5M5Lhw1NwdgatFlq2NPDRR4n3TTi9/LIknBzB1q0uGAwKISHGXF/0c1RaLfTvn8a2bVG8914Cfn4mzp93YuRIPx56qCzbt0uSWQhRdIxG+OUXS6vc8OHJKIplo1yzZpYXvj17bLTKsxSSJJO4K5Pp1kwRSTIJIQRU8qyEgkKqMZWb6TdzvF9pGP6tqvDXX660bx/Ajz96YDIp9OiRxpYt0Ywbd+fspfsZPToZrVZl2zYXDh+WSti8iI9X+OADL9q1C+Dvv90AePTRVLZti+LNN5NyHLJ8v4TTH39IwskRrF1rbZVLLzGtpC4uMGJECjt3RvHyy0m4uZk5fNiZAQPK8uSTfhw9Ks8BQojCt2aNnvBwHX5+Jvr0ubXhtmVLy4vdrl2S+M4vSTKJu7p6VYvBoODiot631cFKkkxCiJLMRetCOfdyAFxKvJTj/Up6kulurXFz5+auNS4nlSqZ6NvX8gZv6lQPW4ZbYmVkwIwZ7rRuHciMGR4YDApt2mSwZk0U334bT4UKuf+3uFvC6ZlnUiThZGcmE2zYUHxb5e7H01Pl1VeT2LkziqFDk3FyUtmyRU/37v6MHu3LhQulp+VYCFH0rAO/Bw9ORa+/9efWJNOePc7SyptPkmQSd2VtlatSxYg2l6/x5xPOA5JkEkKUXNa5TFeSct4wZ00ylbR2ueRkS8VM167ZW+M2bIiiY8eCDyR+4YVkFEVl1SpXzpwpWX93tmQ2Wwast28fwAcfeBMfr6F27Uzmzr3JggU3qV+/YC1V1oTTpEkJknCys4MHnYmN1eLtbc5q3yiJ/P3NfPhhIlu2RNGvXyqKorJsmSsdOgTwv/95ExkpH1eEELZ19KgTe/a4oNNZFpDcrmFDA3q9ys2b2qzPxCJv5Flb3JX1B8q6jvt+DCYDV5OuWh4jSSYhRAkV7Hn/DXNVq1oqSC5eLBlX4a1b49q1C2DGDA+Mxuytcbdf/SuIGjWM9OhhqdaQaqa727XLmYcfLsuYMb5cvaojMNDE5MnxrF0bTceOtt/MJwkn+1q3zlLF1LFjOk6loIMsONjEd9/Fs2ZNNB07pmM0Ksyd606rVgF88oknCQklpF9QCGF3M2daqpgefjiNoCBztttcXKBJE2mZKwhJMom7Oncub5vlriRdwaSacNO5EeQWVJihCSGE3WRtmLtHkslayRQbqy32H4qsrXFjxtiuNe5eXnghGbAkta5cKRlJOls4e1bHM8/48dhjZTl82Bl3dzMTJiSyfXsUTzyRmuuK44L4b8Jp8WJJOBW22+cxlSb16hmZOzeWJUtiaNrUQHq6hqlTPWnVKpAffnAnLe3+xxBCiJxER2tYtswVgGHDUu56n9tb5kTeSZJJ3JW1zSM/Q7+VkjKZUggh/iM3SSYPDxV/f2s1U/Essy7s1ricNGqUSbt26ZhMCt9/L9VMUVEaXnvNm06d/Fm3To9Wq/LUUyns2BHFSy8l4+Zmn2ERWi2EheU+4bRxoySc8uriRS1nzzqh06k8+GDh/cw5spYtDSxdGsPPP8dSs2Ym8fEaPvzQm7ZtA5k/3w1j8V62J4Swk7lz3TAYFJo0MRAamnnX+7RsaXne3bXLReYy5YMkmcRdWdvlZLOcEELckpuZTFB8h3+rKixdqqd9+1utcd27p7F5s21b4+7lxRct1UwLF7oRFVU636akpip8/bUHrVsH8Ntv7phMCt26pbFxYzSffJKAv7/5/gcpIrlJOA0ZIgmnvFq3zvLD1qKFIccNgaWBokC3bumsXx/NV1/FUb68kRs3tLz6qg+dOvmzcqVePgAKIXItIwN+/dXSKjd8eHKO92vSJBMnJ5WICK1UVudD6Xz3Ju4pMVEhKsrywyRJJiGEuKWyZ2UAIlIjSDPm3LNRpUrxm8t0+rSO/v3L8PzzfkRE3GqNmzUrjkqVbN8al5OwMANNmxrIyFD46Sf3IjuvIzAaYd48N1q3DmDyZC9SUzWEhhr4888Yfv45Ltct7PaSU8LJ318STnllTTKVxK1y+aHVwoABaWzbFsW77ybg62vi3DknRozw4+GHy7Jjh7S0CCHub9kyV2JitAQFmejZM+fnV1dXlcaNLS9Qu3fL80teSZJJ3MFaxRQQYMLLK3eXhyTJJIQoDXxdfPFy9gLIWnZwN9alCcWhkik5WeHDDy2tcTt3WlrjJkwo/Na4nCgKvPhiEmC52hgfX/JbsFUV1q93oUsXfyZO9CEqSktwsJHvv49l+fIYWrQofpmY2xNOBw7cO+H0yiuScLpdfLySNQektM1juh+9HkaOTGHXriheeikJNzczhw45079/WQYN8uPYMcd/zhVC2IeqwqxZlotXzzyTct+FCtbX3t27XQo7tBJHkkziDnltlQO4mHARkCSTEKJkUxQlq5rpUuKlHO9XHNrlbm+N++GH7K1xL71UNK1xOencOYM6dTJJSdHwyy8lu5rpyBEn+vcvw9NPl+HMGSd8fMy8914CmzZF8cgj6TbfGGcP90s4LVwoCafbbdqkx2RSqFUrk+DgoqsiLE48PVUmTEhi584onn02GScnlc2b9XTrFsDzz/sUqypSIUTR2LfPmaNHndHrVQYNuvvA79vJ8O/8kySTuIN1s1xuk0zJhmQiUiMASTIJIUq+3MxlcvQk05kzjtEal5Pbq5lmzvQgJaUEZFr+4+pVLS++6EOPHpYKMhcXldGjk9mxI5IRI1JwKaEXTu+WcHr66TsTTqGhpTfhtG6d5R9fqpjuz9/fzEcfJbJlSxR9+6YCsHSpGw8+GMDrr3sTGSkfdYQQFtYW/H79UvHzu3+3TrNmBjQalcuXdVy/Ls8leSF/W+IOea1kuphoqWIq61oWbxfvQotLCCEcQbDn/yeZEu+VZLIkauLjNcTGOk6CxNoa16WLtTVOtWtr3L089FA6ISFG4uM1zJvnZu9wbCY+3vJv0K5dAH/+afm6+vVLZevWKN56KxEfn9IzxdiacPr44zsTTvHxpTPhlJlpqWQCSTLlRXCwialT41mzJoqOHdMxGhXmzHGndesAPv3Uk8REx3keFkIUvWvXtKxebXluHTbs/lVMYNkW3KCBZfvcnj0l9MpPIZEkk7iDNcmU2wGjWfOYvKSKSQhR8lkrme7VLufqqhIUZB3+bf9qpru1xnXrlsbmzVF2b43LiVYLY8ZYNr/MmOFBhmPlwPIsIwN+/NGd1q0D+eEHDwwGhdatM1i9OprvvounYkX7V5DZ038TTosWlc6E0549ziQmaihTxpTjam2Rs/r1jcydG8vixTE0aWIgLU3Dd995EhYWyA8/uJMueTshSqXZs90xmxXatMmgdu3cj4SxtszJ8O+8kSSTyMZkgkuX8lbJJEO/hRClSWUvy0yme7XLgeO0zN2tNW7OnJv8/LNjtMbdy6OPphIUZCIiQsuSJcWzmsma4HvwwQDef9+b+HgNtWplMmfOTRYuvJl1lVTcotVCq1a5Tzht2uRCZgn5a1y71pLx7dw5A62MFcq3sDADy5bFMGtWLDVqZBIfr+HDD71p0yaQBQtcMTr2okYhhA2lpir8/rvlPcSwYcl5emzLlpYrXJJkyhtJMolsrl3TkpGh4OKi5vqqqiSZhBClSVa7XNIVzKo5x/tZN8xduGCfJNPdWuNefdXSGtepU/EoC3Jxgeees7whnDbNo9h9MNy925mHHirL88/7ceWKjsBAE198Ec/atdF06pRRIoZ6F7bcJJwGD741NLw4J5xUFdatk1Y5W1EU6N49nQ0bovnqqzjKlzdy44aW8eN96dzZn1Wr9KilpztViFJr0SJXEhI0hIQY6dw5b+9/mjUzoCgq5845ERMjqZPckr8pkY116HeVKsZcX0GTJJMQojSp4FEBraIlw5RBZGpkjvezVyVTTq1xmzZF8fLLjtkady+DBqXi62vi0iUdK1YUj+DPndPx7LO+PPpoWQ4fdsbNzcyrryayfXsUTz6Zis7+HZTFUklPOJ05o+PKFR0uLirt2hWPRHBxoNXCgAFpbNsWxTvvJODjY+bsWSeGD/fj4YfLsnOnVCgIUVKZzfDzz5aB30OHpqDJY/bD11fNaq+TaqbckySTyMY6j8l6Bf5+VFWVJJMQolTRaXRU9KgIwOXEyznezzr8uyhXaZ85o2PAgLu3xlWu7NitcTlxd1ezhnR+952nQ1ceREdr+N//vOnY0Z+1a13RalWGDElh505Lgs/NzYGDL2byknAaP967WCScrFVMrVtn4O4u3yu2ptfDc8+lsGtXJOPGJeHqaubQIWcef7wsgwb5ceyYZH+FKGm2bnXh3DknPDzM9O+fmq9jWFvm9uyRJFNuSZJJZJPXzXIxaTEkGhJRULKG4QohRElnfb67nHSvJNOtSqbCTowkJyt89JGlNW7HjuLZGncvzz6bgru7mZMnnVi/3vE2vKSmKnz9tQetWwcwd647JpNC165pbNgQzaefJuDvn3NbpSi4nBJOZctaEk4LFrgXi4STdR6TtMoVLi8vlYkTk9i5M4pnnklBp1PZvFlPt24BjBnjw6VLMgxLiJJi1ixLFdOAAal4eubvzZh1+PeuXY73/sNRSZJJZJPfzXKVPCuh1xWPNgYhhCioyp6W4d/3qmQKDjaiKCrJyZpC6+O/vTXu+++Lf2tcTnx8VJ5+2vGqmUwm+P13N9q0CWDyZC9SUjQ0bmxgyZIYfvkljho1itkQqRLg9oTTwYOWhNNTT+WccIqOdoy3wjExGg4edAKgc2dJMhWFgAAzkyYlsGVLFH36WCoc/v7bjfbtA3jjDW+iohzje0MIkT/nzunYuFGPoqgMHZqS7+O0aGFJMp06pSMuToYp5oY8e4ps8lrJJK1yQojSKMQrBIAriTlvmNProXx5a8uc7dswzp7N3hoXHGzk11+Ld2vcvYwYkYKLi8qBA852n4ugqrBhgwtduvgzYYIPkZFaKlc2Mn16LMuXx2Rd9RT2ZU04ffJJzgmnF17wtXeYgOX7SVUVGjQwUL68VL4VpZAQE9OmxbNmTRQdOqRjNCr8+qs7rVoF8NlnniQmyodKIYoj6yymLl3SCQnJ//sif38z1aploqoK+/ZJy1xuSJJJZElMVIiKspQIS5JJCCFyVtnLUsl0KenSPe9Xtart5zKlpFha4zp3zt4at3FjVJ63phQnAQFmBgywVBt8952H3eI4dsyS3HvqqTKcPu2Ej4+Zd99NYPPmKHr3Ts/zUFFRNP6bcPr995s4Oals3+7Ctm32/9BgncfUtatUMdlL/fpGfvstlkWLYggNNZCWpuHbbz0JCwvkhx/cSZd/GiGKjYQEhUWLXAGy5joWhPXi0e7d0jKXG/JWSGSxVjEFBJjw8spdL4IkmYQQpZF1JtO9Kpng1lymCxcKXsmkqrBsmZ527W61xnXtWvJa4+5l9OhktFqVLVv0/PuvU5GeW1Xhp5/c6dnTktxzdlYZNSqZHTsiGTkyBRd531lsaLXQvn0GQ4ZYPnh89pmXXVsw09Nh82bLN1CXLiU3UVxctGplYPnyGGbNiqVGjUzi4zV8+KE3bdsGsHChK0bpghXC4c2f70ZqqobatTNp3brg1cXWJJMM/84dSTKJLHltlQNJMgkhSqdgT0uS6Wb6TZINyTne7/bh3wVhbY0bPTp7a9wvv5TM1ricVK5sok+fNACmTi26aqa0NBg71of33vPGZFLo2TONrVujePvtRHx8HGRAlMizsWOTcXOzbBhbs8Z+WdqdO11IS9MQFGSifn0HnEheCikKdO+ezvr10Xz5ZRzlypm4fl3HK6/40rmzP6tX6x1mNpwQIjujEX75xdIqN2xYCooNOl5btLBcADh61InkZGmhvR9JMoks587lLclkMpu4lHgJkCSTEKJ08XT2xE/vB+R+w1x+pKQoTJrkWepa4+7lhRcsSb1Vq/ScPVv4K8evXdPSp09Z/vzTDa1W5YMPEvjxxzgqVSo9yb2Syt/fzPDh1momT0x2+ie9faucLT4MCdvR6WDgwDS2b4/k7bcT8PExc/asE8OG+fHII2XZtUuqGoRwNGvX6rl2TYevr4m+fVNtcswKFcxUrmzEZFLYv19+7u9HkkwiS14rma4lX8NgNuCidaG8e/nCDE0IIRyOtZrpXhvmrEmmS5e0ebrqfXtr3PTpnqWyNS4nNWsa6dEjDVVVmDatcKuZduxwpkePshw75oyfn4kFC27a7KqocAyjRiXj42PmzBkn/vrLtcjPr6oyj6k40Oth1KgUdu2KZOzYJFxdzRw86Mxjj5Vl8GA/jh0r/IS3ECJ3Zs2yVDENHpyKqw2f1q1b5uy9fKQ4kCSTyGKdGZLXod9VvKqg1dhuqK0QQhQHWXOZknKey1S5sgmNRiU1VUNkZO5ecs+d0zFwoLTG3Yu1munPP125etX2rz+qCjNnuvPEE2WIjdXSoIGB1atjaNVKtsaVNN7eKs8/b/l++vJLTwxF/E987JgTERFa3NzMtGpVOqsTixMvL5XXXktix44onn46BZ1OZdMmPd26BTBmjA+XLsn7YSHs6dgxHbt3u6DTqTz9dMEHft8uLMzyHC1JpvuTJJMAwGS61c5RvbpslhNCiPup7Pn/G+b+v234bpydyWqrut/w79tb47Zvl9a4e2ncOJN27dIxmRR++MG21UxpaTBunA/vvmuZv/Too6n89VcMFSpIgq+kGjo0hYAAE1eu6Pj9d7ciPbe1Va59+4xSXaFY3AQGmvn4Y+tWSUs7zt9/u9G+fQBvvulNdLR8xBLCHmbOtLwn6NUrjXLlzDY9trWS6fBhZ9LSbHroEkeeAQVgSTJNnhzPSy8lUbFi7t5IS5JJCFGahXiFALnfMJfTXCZVheXLb7XGZWZKa1xuWKuZ5s93IyrKNm9nwsO19O1bliVLLPOX3n8/gSlT4m1abi8cj6uryrhxSQB8840nqalF1w+5bp11q5y0yhVHVaqYmD49njVronjwwXSMRoXZs91p1SqAzz/3JDFRemuFKCrR0RqWLrW8YA8bZtsqJoDgYBNBQSYyMxUOHpRqpnuRJJMALFfb+/VLY8KEJLS5rPSVJJMQojSr7GWpZLrX4G+4d5Lp3DkdTzxRhlGjbrXGzZ4trXG50aqVgSZNDGRkKMyc6V7g4+3c6Uz37mU5etQyf2n+/JsMHy7zl0qLJ59MpXJlI9HRWn7+ueDfT7lx/bqGo0edURSVTp2kWrE4q1/fyLx5sfzxRwyhoQZSUzVMmeJJq1YBzJjhTrrkEIUodL/95obBoBAaaqBpU9tv6lQUaNnS8ly9Z48kme5Fkkwi3yTJJIQozayDv68lXcNozrnNuEoVS7Lo4sVbGfyUFIWPP7a0xm3blr01rksX+bCZG4oCL75oqT759Vd3EhLylw2yzl8aOPDW/KVVq2Jo3VrmL5Umzs4wfrzl+2n6dA/i4ws/u7h+vaVMsUmTTMqWtW1bh7CP1q0NLF8ew8yZsVSvnklcnJYPPvCmbdsAFi50tdsGQyFKuowMmDPHcoHAujW0MLRsaR3+7VJo5ygJJMkk8iXNmEZ4cjggSSYhROkU5B6Ei9YFo2rkevL1HO93eyWTtTWuffsApk2ztMZ16ZLOxo3SGpcfnTtnUKdOJsnJGmbPznv1yX/nL/XrZ5m/lNu2cVGy9O2bRq1amSQkaGw+6+tuZKtcyaQo0KNHOhs2RDN5cjxBQSauX9fxyiu+dO7sz5o1+jxtGxVC3N/y5a5ERWkJCjLRq1fhDUyyJpkOHHAq8kURxYkkmUS+XEq8hIqKt7M3fno/e4cjhBBFTqNoqORZCbh3y5w1yXTp0q3WuBs3tFSubGmNmz07luBgSWrkh0ZzazbTzJnueZqlc7f5S99+K/OXSjOtFl57zVLNNHOmu81mfd1NSorCjh2WK+GSZCqZdDp44olUtm+P5O23E/DxMXPmjBNDh/rRu3dZ2VAlhI2oKsyaZbnQ9NRTKTg5Fd65qlc3UqaMifR0Df/+W4gnKuYkySTyJatVzqcqigysEEKUUtYNc5cTc04yVapkQqdTychQslrjxo+X1jhbeeihNEJCjMTGanO9GWznTmd69JD5S+JOXbumExpqIC1Nw7ffFl4109atLmRkKAQHG6lRI3dbfUXx5OoKo0alsHNnJC++mIReb+bAAWcefbQsQ4f6FumgeSFKov37nTlyxBm9XmXw4NRCPZei3NoyJy1zOZMkk8iXrCSTl7TKCSFKr6wNc0k5b5jT6aB+fcsASmtr3CuvJEvFjI3odPD885Zqpu+/97hn+br1aufAgWW4eVNL/foyf0lkpyjwv/8lAvDbb+5cvZrLbSh5ZG2V69IlXZKbpYS3t8r//pfEzp1RPPVUCjqdypo1rrz0kg9mGcklRL5Zl3/07ZtKmTKF/8NkbZmT4d85kySTyBcZ+i2EELmrZAKYPTuWlSujpTWukDz2WCpBQSYiIrQsWXL3aqa0NHjpJR/eeefW/KW//5b5S+JObdoYaNs2g8xMhS+/9LT58U0mWL/ecgW8SxdplSttAgPNfPJJAn/8cRMnJ5UVK1z56ivbf58JURqEh2tZtcqStB82rPAGft/OumFu715njFKIeleSZBL5IkkmIYSAYC/Lhrl7zWQC8Pc306iR7dfpCgsXFxg50lLNNG2axx0bnMLDtfTrV5bFiy3zl957T+YviXt77TVLNdOSJa6cOaOz6bEPHXLi5k0tXl7mrLYLUfq0aGHgs8/iAfj6a0+WLpXND0Lk1ezZbphMCq1aZVCnTtFkfGrXNuLtbSYlRcPx4zKX6W4kySTy5faZTEIIUVoFe1qSTFcSc26XE0Vj8OBUfHzMXLyoY8WKWx/WrPOXjhy5NX9pxAiZvyTuLTQ0kx490jCbFb74wrZVJtZWuQ4d0gt1QK1wfAMGpDFqlCVB/sorvhw6JN8QQuRWaqrC779bWuVGjEgusvNqtdCsmXUuk7TM3Y0kmUSexaXHEZseC0AVryp2jkYIIeynspelXS7BkEBcepydoynd3N1Vhg+3vMn87jtPmb8kCmzixCQURWXlSlcOH7bdh/+1ay1Jpq5dZfC/gDfeSKRz53TS0xWGDfPjxg35eCZEbixZ4kp8vIbgYCOdOhXt82lYmOV8kmS6O3kWE3lmrWIKcg/C3cndztEIIYT9uOpcCXQLBO49/FsUjWeeScHd3cyJE048+mgZmb8kCqRmTSOPPpoGwKefetnkmJcuaTlzxgmtVuXBB2Uek7BURUybFkft2plERmoZOtSPtDQptRTiXqwXkgCefTYFbeHsaMiRtdV5714XGdx/F/lKMhllwlWpZk0yVfOuZudIhBDC/qzDvy8lXrJvIAJfX5WnnrKsL96zxwWtVuXdd2X+ksi/V19NwslJZds2F7ZvL/gVa2urXIsWBnx81AIfT5QMHh4qs2fH4udn4sgRZ8aNk41zQtzL1q0unD3rhLu7mQEDUov8/A0aZOLmZiY+XsOpU7ad21cS5CvJNGLECGbMmMHJkydtHY8oBmTotxBC3GId/i2VTI5hxIhkypY14etr4vffbzJypMxfEvlXqZKJwYMtG4s+/dQLtYB5IWuSSbbKif+qVMnErFlxsnFOiFyYOdNSxTRgQCpeXkWfsNfpbs1l2rNHWub+K19JppYtW7Jnzx7ee+89xowZw4IFC7h27ZqtYxMOSpJMQghxi3X49+XEe2+YE0UjMNDMli1RHDgQSZs2Mn9JFNzYscm4upo5dMg5a55SfiQkKFkfRrp2lSSTuFPz5rJxToj7OX9ey8aNehRF5dlnU+wWh7VlbvduF7vF4KjylWR67rnn+PHHHxk/fjxVq1Zl+fLljB8/nv/973+sXLmS+Ph4G4cpHIkkmYQQ4hZrJZMkmRyHj4+Ki7znEzYSEGBm2DDLB5nPPvPElM/RXps3u2A0KtSokUlIiMwHE3f3341zthw6L0RJ8PPPHgB06pRB1ar2ey4NC7u1Ya6gVa4lTb4Hf+t0Opo3b8748eP56aefeO6553Bzc2POnDmMHj2aTz75hO3bt2MwyFXEksSsmrmYeBGQJJMQQsCtDXOXkyTJJERJNXp0Mt7eZk6fduLvv/M34OvWVjmpYhL3dvvGuaFDZeOcEFYJCQp//GF5Dh42LNmusTRqZECvV4mJ0XL+fBFPHndwNnnGcnNzo2PHjgwaNIjmzZtjNps5fPgw3333HSNGjGDu3Lmkp8sLakkQkRJBmjENnaKjkmcle4cjhBB2F+IZAsD15OsYTHJhRYiSyMdHZfRoyweayZM9yes11MxM2LTJOo+paFdti+JHq4WpU+OoVUs2zglxu19+cSc1VUOtWpm0bWvf91wuLhAaKi1zd1PgJFNUVBRLlizh5Zdf5o033uDEiRN069aNjz/+mM8//5x27dqxatUqpk6daot4hZ1ZW+Uqe1XGSSPlu0IIUda1LG46N1RUriZdtXc4QohCMmxYCv7+Jq5c0fH77255euzevc4kJGjw8zPRpIkko8X9eXpm3zj30kuycU6UbuHhGqZOtbTKjR2b7BBLPVq2lOHfd5OvfXtJSUns3LmTbdu2cfbsWXQ6HU2bNmXQoEGEhoai1d4qFxs2bBhlypRhyZIlNgta2M/5hPMAVPOuZudIhBDCMSiKQrBXMCdjT3Il6QrVfOT5UYiSyM1NZdy4JN56y4cpUzwZMCANV9fcDeKwbpXr3DkDrXRViFyqXNnEzJlxDBhQhn/+caVmTSPjxyfZOywh7OKjj7xJS9PQvHkGvXun2TscAFq0yAA82bXLBVXFIRJfjiBflUwjR47k559/RlEUhg8fzo8//sgrr7zCAw88kC3BZFWpUiW8vLwKHKywPxn6LYQQd6rs+f9zmWT4txAl2qBBqVSqZCQqSsvPP7vn6jGqeivJ1KWLjI8QedOixa2Nc1995cmyZbJxTpQ+u3Y5s2yZKxqNyocfJjhMMueBBzLR6VRu3NBy9apcQbDKVyVT3759adeuHUFBQbm6f9OmTWnatGl+TiUcjCSZhBDiTlkb5mT4txAlmrMzjB+fxEsv+TJ9ugeDB6fg7X3vaqZz53RcuqTD2VmlfXuZxyTybsCANE6fdmLGDA9eftmXypVjaNw4095hCVEkjEZ4+21vAAYPTqV+faOdI7rF1VWlUaNMDhxwZvduZypXdowKK3vLVyVTYGAgGk3OD42KimLLli35Dko4LkkyCSHEnYI9/z/JJJVMQpR4/fqlUbNmJvHxGn74weO+97dulWvdOgN3d9lzLfLnzTcT6dRJNs6J0ue339w4edIJHx8zEyYk2jucO4SFWS4eyPDvW/L17DR9+nTOnDmT4+3nzp1j+vTp+Q5KOCaDyZA11FaSTEIIcYu1kulK0hU7RyKEKGxaLUycaJmLM3OmO9HR9347vW6d5YOHtMqJgtBqYdo0+26cM5th505njh/PVzOMEHkWG6vhiy8sY3cmTkzEz8/xEvUtWsjw7/8qlBR4enr6XWczieLtStIVTKoJN50bgW6B9g5HCCEcxu0zmVTV8d4ACSFsq3v3dBo3NpCaquHbb3OuZrp5U8P+/ZYPHp07FzzJpKoqkamRGEyyoa408vRU+eWXWxvnXn7Zh6J4yYmI0DBligdhYQE8/nhZevcuS2ysVFKJwvfZZ57Ex2uoWzeTwYNT7R3OXTVrZkCjUbl0SScVhv8v12noy5cvc+nSpazfnzx5EpPJdMf9UlJSWLduHeXKlbNJgMJx3N4qpzjKtDUhhHAAlTwroaCQakwlJi0Gfzd/e4fk0C4lXuLNHW/i6ezJ9I7T0SjypkwUL4oCr72WyBNPlGXuXHeeey6FihXvfF+8fr0LqqpQv76BChUKvn9+xtEZfLjnQ3SKjireVajhU4MavjWo6VOTGr41qOZdDb1OBkOXZMHBtzbOLV/uSs2ambzySrLNz2M2w5YtLsyb58batXpMplvv/dPSNKxd68LAgcVn/ozJZPmanJzsHYnIraNHnZg3zw2Ajz5KcNjNnJ6eKvXrZ3LkiDN79rjQp0/x+bkoLLlOMu3du5fFixdn/X79+vWsX7/+rvd1c3PjhRdeKHh0wqFYk0yynlsIIbJz1jpT3qM84cnhXEq6JEmme1h1cRWvbH2FRINlrsKAmgPoUKmDnaMSIu/atTPQunUGO3a48OWXnnz9dfwd91m/3rpVzjYDv/889ycARtXI2fiznI0/C5du3a5RNFT2rExN35qWBJRPDWr61qS6T3XcnXK3DU84vhYtDHz6aTzjx/vy5ZdeVK9u5JFHbNOOGRGhYeFCN37/3Y1r1259VHzgAQODB6dw/ryO777zZMUK12KTZIqNVejcOYDkZIX27TPo0iWdTp0yKFOm4IlfUThUFd56yxtVVejTJzWrJc1RtWhh4MgRy/BvSTLlIcnUuXNnmjZtiqqqvPHGG/Tv35/Q0NA77qfX6wkMDJR2uRJIhn4LIUTOgj2DCU8O50riFZoFNrN3OA4n05zJx3s/5sejPwLg7uROSmYKc07OkSSTKLb+979EHn7Yn8WLXRk9OpmaNW9tPUpPh82bLfOYunYteAIgOjWa4zePA7Cm3xpupt3kTNwZzsafzfpvfEY8lxIvcSnxEmsvr832+IoeFbMln2r4Wv7r7eJd4NhE0Rs40LJx7scfLRvngoNjaNQofxvncqpa8vY289hjqTz5ZCq1a1u+t8+csSSZtm93ITFRwcvL8VvEV650JTJSm/X/K1e6oigqTZtm0rlzOl26pFOrlhFp1HAcf/7pyv79zri5mXnrLccb9v1fYWEGfvoJdu+WuUyQhySTr68vvr6+ALz77rtUqFABb295USpNJMkkhBA5C/YKZueNnbJh7i7Ck8MZvWE0B6IOAPBcg+d4vObjdF7SmfVX1hOeHE4Fjwp2jlKIvGvSJJNu3dJYs8aVL77w5Kef4rJu27XLhdRUDUFBJho0KPi6+W3XtwFQv0x96pepD0D7iu2zbldVlZi0GM7En+Fs3FnOxJ/JSj7FpMVwLfka15KvsfHqxmzHDXILytZyZ61+8tP7FThmUbjeeiuRc+d0bNyoZ+hQP/75J5py5XJfnRMZqWHBgpyrlh56KB1X1+xJpJo1jVSvnsm5c05s2KCnb1/Hr9r45x9XAJ56KoUyZcysW+fCsWPO7N9v+fXpp15UqmSkS5d0unTJoGXLDJwlV2A3yckKkyZZhn2PHZucp+9pe2nWzFKtevasEzExGsqWdfyYC1O+VgPUrVvX1nFks3r1apYvX058fDzBwcEMHTqU6tWr3/W+e/bs4a+//iIiIgKTyURQUBAPP/ww7dq1A8BoNLJgwQIOHTpEVFQUbm5uNGjQgCeffBI/v1svnsnJyfz8888cOHAARVFo0aIFzz77LHq99LVbSZJJCCFyZt0wdzlJkky323x1My9seoG4jDi8nL34uv3XdA/pDkBYuTB23djF76d+Z8IDE+wbqBD5NHFiEmvX6lm50pV//03OqiZZu9byHrJz53SbVEhsubYFyJ5Yup2iKPi7+ePv5k/r8q2z3RabHsu5+HOciTuTLQkVkRJBRKrl17bwbdkeU0ZfJqvyKasCyrcGAa4BMpvTQWi1MH16HI88UpYzZ5wYNsyPJUtu3pEYup3ZDFu3uvDbb9mrlry8LFVLgwbdqlrKSY8e6Xz3nRMrVzp+kunmTQ07dlgyRqNGJRMcbOLVV5O4fl3D+vV61q3Ts2OHC1ev6vj5Zw9+/tkDDw8z7dtn0LmztNXZw5QpHkRGagkJMTJypO3njRUGPz+VOnUyOXnSiT17nOnVq3RvE81Vkun9999HURTefPNNtFot77///n0foygK77zzTp4D2rlzJ3PmzGHEiBHUqFGDFStWMGnSJL755pu7Vk55eHjQr18/ypcvj06n4+DBg0yfPh0vLy8aN26MwWDg4sWLPProo4SEhJCcnMzs2bP5/PPP+fTTT7OO8+233xIXF8dbb72FyWRi+vTpzJgxg3HjxuX5ayiJkg3JRKZGAlDFq4qdoxFCCMdj3TB3JfGKnSNxDCazia8OfsWUQ1NQUalfpj4/dv4xKxkH8FSdp9h1YxfzT8/npSYv4aSRiayi+Kld20i/fmksWeLGp596Mn9+LKoK69ZZ5zHZZqvc1mtbAWhXoV2eH++n96N5UHOaBzXP9ueJhkTOxp21JKCslU9xZ7mafJWb6TfZdWMXu27syvYYb2fvbJVP1v+Wdy8vySc78PRUmT07ll69yvLvv5aNc99/H3dHYtNatTR/vhtXr2avWho0KIWHH76zaiknvXql8913nmza5EJampLrx9nDqlV6zGaFBg0MBAffGs5fvryZp55K5amnUklNVdi2zYX1611Yv15PVJSWFStcWbHiVludpcopnZo1pa2uMJ0/r+WnnywbO997LwEXFzsHlActWhgkyfT/cpVk+u86ZlVV7/sikt8Vzv/88w+dOnWiQwfLfIYRI0Zw8OBBNm3aRJ8+fe64f7169bL9vmfPnmzZsoVTp07RuHFj3NzcePvtt7PdZ+jQobzxxhvExMRQtmxZrl27xuHDh/nkk0+oVq1a1n0++eQThgwZkq3iqbS6mHgRgLKuZaV3Xwgh7iLEKwSQSiawzI55YdMLbL++HYAhdYbwXsv37th61T2kO/6u/kSmRrLm0hoeqvqQPcIVosDGj09i6VJXtm7Vs2OHM97eZm7c0OLqaqZ164IP/T4Vd4qotCj0Wj3Ngmw3883L2YumgU1pGtg025+nZqZmJZ7Oxp3Nmvt0OekyCYYE9kfuZ3/k/myPcXdyzzZs3Prfih4V0WpkVmthsm6cGzgw+8Y5W1Qt3U39+plUqmTk6lUdmze70KOH436gtrbKPfxwzjG6ual065ZOt27pmM0JHDnixLp1liqn48edstrqPvnEi8qVjf8/x0na6mxNVeG997zJzFTo2DHdZgsTikqLFhnMnu3O7t3FKDNWSHKVZHrvvffu+XtbMRqNXLhwIVsySaPR0KBBA86cOXPfx6uqyrFjx7h+/TqDBg3K8X6pqakoioKbm2Ul4pkzZ3B3d89KMAE0aNAARVE4d+4czZs3v+MYmZmZZGbe6q9XFAVXV9fcfJnFUlarnJe0ygkhxN1YK5kiUyNJM6bhqiu5rwn3sufGHkZvHE1kaiRuOjc+b/s5fav3vet9nbXODKw1kO8Of8fck3MlySSKreBgE4MGpfLrr+58+qkXHTpYPtC2a5eBLd4eWquYWpVvhYu28D/AuDm50dC/IQ39G2b783RjOhcSLmQlnc7En+Fc3DkuJFwgJTOFw9GHORx9ONtj9Fo91XyqUb9MfYbWH5o1T0rYVsuWBj75JIFXX/Xhyy+9iIjQsnWrS4Grlu5GUSwtcz/+6MHKlXqHTTLd3ir30EO5a+vTaKBx40waN85kwoQkwsM1bNhwq63uypU72+qs2+r8/KStriDWr3dh40Y9Tk4q772XYO9w8qxlS8sGvBMndMTHK/j4OG6FX2HL10ymwpKYmIjZbMbHxyfbn/v4+HD9+vUcH5eamspzzz2H0WhEo9EwbNgwGjZseNf7GgwG5s2bR+vWrbOSTPHx8Xh5eWW7n1arxcPDg/j4+Lse56+//mLx4sVZv69SpQqfffZZLr7K4ul8/HkAqvlUu889hRCidPLV++Lt7E2CIYEriVeo5VfL3iEVKbNq5ocjP/Dpvk8xqSZq+tTkx84/UsO3xj0fN7j2YKYensr269s5H39eXmdEsTVuXBILF7py8KAzZ89a3mLbYqscwNbw/LfK2ZJep6dumbrULZN9PmumOZNLCZeyDRs/E3eGCwkXSDelc/zmcY7fPM7CMwvpGdKT8U3HU9uvtp2+ipLriSdSOX1ax08/eTBvnjtwq2rpySdTqVMn71VLOenZ05JkWrdOj8GAQ1b05NQqlxcVKtzZVrdunaWtLjpa2upsJT3dUsUEMHJkMtWq5e/fy54CAsxUrWrkwgUd+/Y5F7tKLFvKVZIpJiYmXwcvW7Zsvh6XV3q9ni+++IL09HSOHj3KnDlzCAwMvKOVzmg08vXXXwMwfPjwAp2zb9++PPTQrSuuJb0HXYZ+CyHE/VX2qszRmKNcTrpcqpJMcelxvLTlJdZfWQ9Av+r9+KzNZ7g5ud33sRU9K9KpcifWX1nP3JNzeS/svUKOVojCERhoZtiwFKZN8yQpSYOiqHTqVPAPGenGdHbf2A3kPPTb3pw0TpbNdL416FWlV9afm8wmriRd4UzcGZZdWMbS80tZeWklqy6t4pFqj/BKk1eo7nP35T4if95+O5HUVIVLl3Q89lhqgauWctK0qYGAABNRUVq2b3ehY0fH+0Cdm1a5vMhrW12XLul07pxOy5YGh0zCOZKffvLg0iUdgYEmxo4tHsO+7yYsLIMLF3Ts3u0iSab7GTNmTL4OvnDhwjzd38vLC41Gc0f1UHx8/B3VTbfTaDQEBQUBEBISQnh4OH///Xe2JJM1wRQTE8M777yTVcUElkqpxMTEbMc0mUwkJyfneF4nJyecnErPgFJJMgkhxP0FewZbkkyJpWcu0+Howzy3/jmuJV/DRevCh60+5MlaT+bp4suQOkNYf2U9i84u4rVmr5XaVkNR/D3/fDJz57qTmKghNDQTf/+Ct8/sjdxLuimdIPcgavjcuzLQ0Wg1Wqp4V6GKdxW6hXTjxcYv8tXBr1hxcQVLzy9l+YXl9K3Wl5ebvEwVb1ksYwtaLXz+eeG3Gmk00L17OnPmuLNqld7hkkz5aZXLi7u11a1fr2f9+lttdbNmeTBrlrTV3c/16xqmTLEM+37zzUQ8PIpvm1mLFgbmzXNnz57SnVXMVZJp9OjRhR0HADqdjqpVq3Ls2LGsOUhms5ljx47RvXv3XB/HbDZnm5dkTTBFRETw7rvv4unpme3+NWvWJCUlhQsXLlC1qiWJcuzYMVRVpXp1ubqiqqokmYQQIhesm9OuJJX8DXOqqjL7xGze3/0+meZMQrxCmNFpBvXL5n3eSoeKHajoUZFryddYfmE5/Wv2L4SIhSh8Pj4qEyYk8fbb3jz5ZKpNjnn7VrniXjlf2682P3b+kWM3j/HVga9Yc3kNS84t4e/zf/N4jcd5qclLVPKsZO8wRS717JnGnDnurF6t55NPEtA50CAWW7TK5UWFCmaefjqVp5++d1udRqPStKmBLl0sSacaNaStbtIkL9LSNDzwgIF+/WyfECxK1rlMR444kZKi4O5efBNmBZGrp4IHH3ywkMO45aGHHmLatGlUrVqV6tWrs3LlSjIyMrJimDp1Kn5+fjz55JOAZTZStWrVCAwMJDMzk0OHDrFt27asdjij0chXX33FxYsXee211zCbzVmVUh4eHuh0OipWrEjjxo2ZMWMGI0aMwGg08vPPP9OqVSvZLAfEpMWQlJmEgpJt9bQQQojsrM+RlxIv2TeQQpZsSGbCtgksu7AMgB4hPfiq/Vd4OXvd55F3p9VoGVxnMJ/u+5Q5J+dIkkkUa0OHptCvXyre3rb5cLHl2hYA2ldwzFa5/Khfpj4/d/2Zf6P/ZfKByWy8upEFZxaw+OxiBtYayNjQsVTwqGDvMMV9tGxpwMfHTGyslr17nWnVymDvkLJYW+Ueeqjoh5L/t63u339vtdWdOOHEvn0u7NvnwscfexEcbNlWV1rb6nbvdubvv91QFJWPPkoo9gm3ChVMWZsX9+93pn17x6rwKyoOlG+2aNWqFYmJifzxxx/Ex8cTEhLCG2+8kdW2FhMTk+0qTkZGBjNnzuTmzZs4OztToUIFXnzxRVq1agVAbGws+/dbVqxOnDgx27nefffdrJa6sWPHMmvWLD744AMURaFFixYMHTq0CL5ix2etYqrkWalINpoIIURxZd0wV5IrmU7GnmTk+pFcSLiATtHxZos3GVF/RIErLAbWHMiXB77kUNQhjsUcy1dFlBCOwlZbhaJSozgRewKAthXa2uSYjqSRfyPmdp/L/sj9TD4wmW3h2/jt1G/8ceYPBtUexIuhLxLoFmjvMEUOnJygW7d0Fi50Y+VKvcMkmW7e1LBzZ+G1yuWFRgOhoZmEhmYyceKdbXWXL99qq/P0vNVW17FjyW+rMxrhrbcsw74HDUqlQYPM+zyieGjRwsDVqzp27y69SSZFVdX7vgpat6j169cPjUaTbavavTz22GMFi64Yio6OztaqVxLMPzWfV7e9yoMVH2Rej3n2DkcIIRzWlcQrhC0Mw0Xrwrlnz6FRNPYOyaYWnlnIG9vfIN2UTjn3cnzf6XuaBTaz2fFHbxjNsgvLGFR7EJ+3/dxmxxWiuFpydgljN4+lQdkGrO672t7hFLrdN3Yz+cBkdt3YBYBeq+epuk8xptEYyroWzUIhkTfr1rnwzDNlCAoysW9fJBoHeNn77Tc3XnvNhwYNDKxenb8FVkUhJeVWW92GDZa2OqvS0FY3e7Ybb77pg4+PmW3bokpMUm3BAlfGj/elefMM/vrrpr3DsSknJyf8/f3ve79cVTItWrQIgD59+qDRaLJ+fz+lMclUEp1POA9ANW9ZKy2EEPdS3qM8OkVHhimDiJQIynuUt3dINpFmTOOtHW+x4MwCAB6s+CDfdfgOP71tW8qfqvsUyy4s469zf/F2i7fxdPa8/4PEPaUZ05h1bBazT8zG39Wf/jX706daH3z1vvYOTeRCSWyVu5eW5VqyqNcitl/fzhf7v+BA1AF+PPojc0/OZWi9oYxqOMrmzzuiYNq2zcDDw0xEhJZDh5xo2tT+F9vt2SqXF+7uKt27p9O9e+lrq4uNVfjiC0uL/YQJiSUmwQSWSiaAw4edSUsD11K4yyRXlUwi90piJdPQtUNZc3kNk1pN4pl6z9g7HCGEcGitF7bmUuIlFj+0mLByYfYOp8DOx5/nuQ3PcTL2JBpFw/gm4xkbOrZQqrRUVaXj4o6ciT8jrzkFZFbN/H3+bz7d9ynhyeHZbnPWONM1uCsDag2gfYX2aDXaHI4i7ElVVULnhRKdFs0fvf6gdfnW9g6pSKmqyuZrm/li/xf8G/MvAO5O7gyvP5znGjyHt4u3nSMUVs8/78PSpW6MGpXM228n3v8BhejmTQ2hoYGYTAo7dkQSElL4Q78LQ3i4NqvCaccOFzIybpUxZW+rS8fPr/h9nP/f/7yZO9edOnUyWb062qGGxheUqkLTpoFERmpZtCjGYdpIbSG3lUwOUNAoHJ1slhNCiNwL9vz/DXOJxX8u0/ILy+n5d09Oxp6krGtZ5veYz0tNXiq0NkBFURhSZwgAc07OQa6D5c+eG3t4eOnDvLjpRcKTwynnXo6v2n/FB2EfUK9MPQxmA/9c/Ichq4fQfH5zPt77Mefiz9k7bPEfJ2NPEp0WjavOlQcCH7B3OEVOURQ6VOrAij4r+KXrL9QrU4+UzBSmHJpCywUt+frg1yQZkuwdpgB69rRUDK1apcfeT9urVukxmSxb5YprggksA6SfeSaVuXNjOXo0glmzYhk4MIWyZU0kJWn45x9Xxo3zpVGjIPr2LcO0aR6cOaOz+99/bhw7puO339wA+PBDx9pKaAuKAi1bWmYx7dlTzEvO8ilf7xIHDBjA9u3bc7x9586dDBgwIN9BCcdhMpuytiRJkkkIIe6vspdl+Hdx3jBnMBl4e+fbjNowiuTMZFoGtWRN3zW0qdCm0M/9aI1HcdW5cjruNPsi9xX6+UqSCwkXGL5uOP3+6cfh6MO4O7nz2gOvsa3/NgbUHMCw+sNY228ta/qtYVj9Yfi6+BKRGsG0f6fRflF7Hln6CPNOzZMP7g5ia/hWAMLKhZXqxSuKotA1uCur+67mp84/Udu3NomGRCYfmEzLBS2ZengqKZkp9g6zVOvYMQO9XuXyZR3Hj9s3Y1BcWuXywtpW9+WXCRw6FMny5dGMHZtE3bqZmM0Ke/daWuo6dAigdesA3nnHi23bnDE4YAGNqsLbb3ujqgqPPJJGWJgDBmkD1pa53btL53N3oVyKNJvNBd4yIxzDteRrZJozcdG6lJjZIkIIUZhCvEKA4rth7lrSNfot78fPx38GYEyjMSzstZAg96AiOb+3izd9qvUBYM6JOUVyzuIuLj2Od3a9Q4dFHVh1aRUaRcPg2oPZ0X8HY0PH4qrLPhCifpn6fBD2AQcHHeSnzj/RuXJntIqWA1EHmLhtIo1/a8yLm15ke/h2zGrJmZNR3GTNY6pYOuYx3Y9G0dCzSk/WPbqO6R2nU827GvEZ8Xyy7xPCFoTxw5EfSDPad5NYaeXmpvLgg9ZqJvsNoHGkrXKFRaOBJk0yee21JNati2bv3kgmTYqnQ4d0nJ3VrG11AweWpWHDIEaN8mXJEldiYx3js/nff7uyd68Lrq5m3norwd7hFBpr8mz/fieHTPYVNpsnmVJTUzl8+DCenjKssySwtspV8apS4rYkCSFEYajsaalkupx42c6R5N36K+vp9lc3DkUfwsfFh9ldZ/NG8zfQaYr2yvRTdZ4CYMXFFdxMK1mbWWwpw5TBjCMzaL2wNbOOzcKoGulYqSPr+63ns7af4e9277kJzlpnelbpya/dfmXfk/t4q/lb1PCpQbopnT/P/cmAlQMIWxDG5AOTS0T7Z3GSZkxjT8QeoPQM/c4tjaKhd7XebHxsI1MenEKIVwg302/y4Z4PabWgFbOOzSLdWHKqWIoLa8vcypV6u8VQUlrl8sLaVvfbb7EcOxbBzJnZ2+qWL3dl7NhbbXXTp3tw9qx92upSUhQ++sgy7PvFF5OpUKHkXsSoUcOIn5+J9HQNR4442TucIpfrwd+LFi1i8eLFuT5wjx49eOaZZ/IbV7FV0gZ/zzo2i3d2vUPPkJ781OUne4cjhBAO7/jN43T9syt+ej+ODjlq73ByxWg28sWBL5h6eCoAjf0b80OnH6jkWcluMfX6uxeHow/zRrM3GNN4jN3icESqqrLi4go+3vsxl5Msycw6fnV4p8U7tKvYrsDHPhR9iD/O/MHS80tJNNwa4htWLowBNQfQq0ov3JzcCnQecW9brm3hyVVPUs69HPue2CcdAveQac5kydklfH3wa64lXwOgnHs5xjYey8BaA3HWls6ZKEUtIUGhUaMgMjMVtmyJonp1Y5HHMHBgGbZtc+H11xN54YXkIj+/IzGb4fDhW9vqTp7MnugICTHSqVM6ffqkERqaSVE8xXzyiSdTp3oSHGxk48Yo9PbLRxaJESN8WbnStUR9P+Z28Heuk0yHDh3i0KFDqKrK2rVradiwIeXKlbvjfnq9nqpVq9K8eXM0mtJX+VLSkkxv7niT2Sdm80LjF3i92ev2DkcIIRxesiGZWr/WAuDU06fwdHbsyt7I1EjGbBzDrhu7AHi27rO83fJtu8+AWXB6AeO3jqeyZ2V2DNgh1bT/72DUQT7Y/UHWvKpAt0AmPjCRx2s8bvMtcWnGNNZcWsPCMwvZFr4NFctbRg8nDx6u+jD9a/anWWAzSYAUgg92f8CMozMYWHMgX7b/0t7hFAsGk4EFpxcw5fAUIlIiAKjoUZGXQl/isZqP4aQpfdUERW3wYD82bdLz2muJjB1btB+qY2M1NG5c/LfKFZZr17SsX+/C+vWWbXUGw63n7bp1Mxk0KIV+/dLw8iqcEqcLF7R07BhAZqbCL7/cpGvXjEI5jyOZOdOdd9/1pmPHdObOjbV3ODZh8yTT7aZPn06XLl2oUaNGvoIryUpakmngyoFsC9/GV+2/YkBNGeYuhBC50XBuQ26m32RNvzXUL1Pf3uHkaMf1HYzZOIbotGjcndz5ou0X9K7W295hAZYER9N5TUkwJPBb99/oUKmDvUOyq6tJV/l478csu7AMAFedK6MbjmZUw1G4O7kX+vnDk8NZdGYRi84uyjbUvopXFfrX7M9jNR6T2Y021HlJZ07GnmR6x+kO8zNZXKQb0/n91O98d/g7otKiAMusvJdCX6Jf9X42T8aKW+bNc2PiRB8aNjSwalWMXc7doIGB1auL9tzFTUqKwtatLqxcqWflSlfS0y0JJ1dXM717pzFoUKrNq5ueesqPDRv0dOhgSbiUhmsTx47p6NYtAA8PMydORKAtAU89hZpkEjkraUmm5vObE54czt+P/E2zwGb2DkcIIYqFh5Y+xKGoQ/zY+Ud6Vell73DuYFbNfHf4OyYfmIxZNVPHrw4/dPqB6j7V7R1aNu/seodZx2bRNbgrv3T9xd7h2EVCRgLfHf6OWcdmYTAbUFDoX7M/Ex6YQDn3OyvKC5uqquyJ2MMfZ/5g+YXlpBpTAcucnHYV2tG/Zn+6BXdDryvhfRCFKDI1kibzmqCgcGTIEfz0fvYOqVhKM6Yx58Qcpv07jZvpltlu1byrMb7peB6u+rBURxaCmBgNoaGBmM0Ku3dHUqlS0VUTSatc/sTFKfz5pxu//ebGmTO3qv1sWd20fr0LTz9dBicnlfXro6he3XGrzFRVtVl1rskE9esHkZioYdWqaBo2LP45gtwmmQr07Hrz5k3279/P1q1b2bJlyx2/RPGWZkwjPDkcsLwoCyGEyJ0QzxAAhxyWHJsey1Orn+Lz/Z9jVs0MqDmA5b2XO1yCCW4NAF9/ZX3W61FpkWnO5Jfjv9B6YWu+P/I9BrOBNuXbsLrfar5q/5VdEkxgWSffslxLvmr/FYcHH+ar9l/RMqglZtXM5mubeX7j8zSZ14TXt7/O4ejDyLXMvNt6bSsADcs2lARTAbjqXHmu4XPsGriLN5q9gY+LD+cTzvP8xufpvKQzKy6ukO2JNla2rDlrdfuqVUWXaI6NLflb5QqLr6/KsGEpbNwYzd9/x/Doo6m4uKicOOHEm2/60KRJIOPHe3PokFO+hoVnZMC773oDMHx4ikMnmP658A/Vf6nO0vNLbXI8rRaaNbP8POzeXbpmw+VrXYzBYGDatGns2bPnnm8e2reXbRjFmbUc3sfFB18XX/sGI4QQxUhlL8uGudvbihzBgcgDjNowiusp19Fr9UxqPYmBtQbaO6wcVfepTli5MHbd2MXvp35nwgMT7B1SoVNVlXVX1vHRno84n3AegBo+NXirxVt0qtTJoeYfuTu5M6DmAAbUHMClxEv8ceYPFp1ZxPWU68w5OYc5J+dQy7cW/Wv259Hqj953252w2BpuSTIVdIi7sHB3cmdM4zE8VfcpZh6byY9Hf+R03GlGrh9JXb+6THhgAl0qd3Gon63irGfPdHbtsrRijRyZUiTnLI1b5WxNUSwJkWbNDLz/fgJLlrgxb56lumnBAncWLHCnbt1MBg9OoW/f3Fc3/fSTB5cu6QgIMDFuXFIhfxX5ZzKb+Hjvx6Sb0ll4eqHN2pTDwjLYsEHP7t3ORfbz4AjyVck0f/589u7dy8CBA3n33XcBGDNmDG+++SahoaGEhITwxRdf2DRQUfQuJFwAoIp3FXnhFUKIPAj2CgbgSpJjVDKpqspPR3+i3/J+XE+5ThWvKizvvdyhE0xW1mqm+afnk2ku/qXm93I05iiPr3icZ9c+y/mE85TRl+Hj1h+z/tH1dK7c2aFfi0O8Qpj4wER2D9zN/B7z6VutL3qtntNxp/lwz4c0/b0pz659ltWXVmMwGewdrsMyq2a2hW8DoF0FSTLZkqezJy83eZndA3fzUuhLeDh5cCL2BM+ufZZef/di49WNUnlnA927WyqJ9u93JjKyaFoSly93BeChh9KL5Hwlna+vyvDhd69ueuMNS3XTq6/ev7rpxg0NU6Z4APDmm4l4ejruz9eKiyuytrXui9xns/cb1sq+PXtcMJeiwsl8/eTv3r2bBx98kD59+lCpkmW9sZ+fHw0bNuR///sfbm5urFmzxqaBiqJnTTJV9apq50iEEKJ4CfZ0nCRToiGRkRtG8t7u9zCqRh6q8hCr+q6ibpm69g4tV7qHdMff1Z/I1EjWXCqZ7y2uJ19n3OZx9PirB7tu7MJF68ILjV5gx4AdPF33aXSafBWe24VWo6VdxXZM7TiVg4MO8mmbTwkNCMWkmlh7eS3D1g3jgd8f4N1d73Li5gl7h+twTsaeJDotGjedG00Dm9o7nBLJ28WbCQ9MYNfAXbzQ6AVcda78G/MvQ1YPofey3mwN3yrJpgIoX95MaKgBVVVYvbrwW+akVa7wWKubvv02ngMHInj//QRq1MgkLU3D/PnuPPSQP926+fPrr24kJt55EWTSJC9SUzU0bWqgXz/H/bdRVZXvj3yf9ftUYypHY47a5NgNGmTi5mYmPl7D6dPF57W8oPKVZEpMTKR6dcvsBmdnyw91evqtzHGLFi3Yu3evDcIT9mRNMlXzkXlMQgiRF9ZKpmtJ1zCajXaL49jNY/T4qwcrL67ESePER60+4odOP+Dp7Gm3mPLKWeucVXE19+RcO0djW8mGZD7f/zlt/2jL4rOLUVHpV70fWx/fyuvNXy9W/0534+3izZA6Q/in9z9semwToxuOJsA1gJvpN5l5bCZd/uxC97+688vxX4hLj7N3uA7BOo8prFwYLloXO0dTsvnp/Xi9+evsHrib5xo8h16r50DUAZ5Y+QSP/fMYu2/stneIxVbPnpbPhatWuRb6uaytcvXrS6tcYbJWN23aFM1ff92qbjp+/O7VTXv3OvPXX24oispHHyWgceA5+9uvb+dIzBH0Wj3NA5sD2Ozn38kJHnjAWs1UeuYy5euf29vbm6QkS0+li4sL7u7uXL9+Pev2tLQ0DAYphS7uzsdbZkFU9ZZKJiGEyItAt0BctC4YVSPXk6/f/wE2pqoqv5/6nUeWPsKlxEtU8KjAXw//xbP1nnXolqucDK49GAWF7de3Z702FWdGs5F5p+bR5o82TDk0hXRTOi2CWrCizwq+6/AdFT0r2jtEm6vpW5O3WrzFvif3MbvrbHpW6YmTxomjMUd5a+dbNJnXhJHrR7Lhyga7JmbtbUu4ZXFO+4oy17SolHUtyzst32HnwJ0MrTcUZ40zuyN28+g/jzJgxQD2R+63d4jFTo8elqqVnTudiY0t3Necf/6xVEs9/LC0yhUFRYHmze9f3fTKKz4APPlkqsNvVfv+X0sV0xO1nqBHlR4A7Lqxy2bHt7bM7d5dei4c5CvJVL16dU6dOpX1+6ZNm7J8+XK2bdvG1q1bWbFiBTVr1rRZkMI+strlJMkkhBB5olE0VPb8/+HfSZeK9NypmamM2zyOCdsmkGHKoFOlTqzpu4bQgNAijcOWKnpWpFPlTkDxr2bafHUz3f7sxsRtE4lOiybEK4SZnWey5KElNPZvbO/wCp1Oo6NLcBd+6vwTBwcd5IOwD6hXph4Gs4EVF1fw1JqnaDG/BR/v/Zhz8efsHW6RSjOmsTfC0gkgSaaiF+gWyIetPmTHgB0MqTMEJ40T269vp/ey3gxZPYTD0YftHWKxUaWKiTp1MjGZFNatK7yWudhYDTt2WD64S6tc0btXddPFizq8vc289prjDvsGOBZzjC3hW9AqWp5r+Bxh5cIA2BexD5PZNpVxYWG3NsyVlk7cfCWZevbsSWBgIJmZlqzkgAEDcHNzY+rUqUybNg03NzeeffZZmwYqilZseixxGZbS9SpeVewcjRBCFD/WJNOVxKKby3Qu/hwPLX2IJeeWoFE0vN7sdWZ3m42vvvhvCLUOAF90dhFpxuL3YeJk7EkGrRrEoNWDOBV3Ch8XH94Pe59Nj22iR5UexbLCrKD89H4Mqz+Mtf3WsqbfGobVG4aviy8RqRFM+3ca7Re155GljzDv1DwSDYn2DrfQ7bmxhwxTBuXdy1PNW0YV2Et5j/J82uZTtvXfxhO1nkCraNl4dSO9/u7Fs2uf5djNY/YOsVjo1cvyPL1yZeG1zEmrnGO4W3VTmzYZfP11PGXKOPa06+lHpgPwcNWHqeRZibp+dfFy9iIpM4njN4/b5ByNGhlwcVGJjtZy4YLWJsd0dPmaPlW7dm1q166d9fuyZcvy9ddfc+XKFTQaDRUqVECrLR1/gSXVxYSLAJRzL4ebk5udoxFCiOInxCsEgMuJl4vkfH+f+5sJ2yaQakwlwDWA6Z2mZ12RKwkerPggFT0qci35GssvLKd/zf72DilXolKj+GL/Fyw4swCzasZJ48Sz9Z5lXOg4fFx87B2ew6hfpj71W9XnrRZvse7KOv448webrm7iQNQBDkQd4J2d79CzSk/61+xP6/Kt0SgOPOAjn25vlSuNSUdHU8mzEpPbTWZMozF8c+gb/jz3J2svr2Xt5bX0rNKT8U3GU9uv9v0PVEr16JHO5MlebN3qQnKygoeH7Us4pFXO8Virm4YPT7F3KPd1JfEKyy8sB2B0o9GAZXlFs8BmbLi6gV03dtHQv2GBz6PXQ2iogd27Xdi924Vq1VILfExHZ7NXaI1GQ0hICJUrV5YEUwkgrXJCCFEwlb0slUzWlbiFJcOUwevbX2fMpjGkGlNpVa4Va/qtKVEJJrC88RtcZzAAc07OsXM095dmTOPrg1/TemFrfj/9O2bVTK8qvdj8+GbebfmuJJhy4Kx1pleVXvza7Vf2PbmPt5q/RQ2fGqSb0vnz3J8MXDmQsAVhTD4wuUirBIuCdeh3uwrt7ByJuF0V7ypMeXAKmx7bRO9qvVFQWHlxJZ2XdOb5jc+XurbO3KpVy0jVqkYMBoUNG2w/i0Za5URBzTg6A7Nqpn2F9tQvUz/rz63vn3ZH2G74f8uWlpa5EyecbHZMR5arSqYTJ/K3YrZu3eKxHlncSZJMQghRMMGelg1zhVnJdDnxMqM2jOJIzBEAxoWOY3yT8Wg1JfNizxO1nuDLA19yKOoQx2KOUb9s/fs/qIiZVTOLzy7ms/2fEZESAUBoQCjvtniXZkHN7Bxd8RLoFsjoRqMZ1XAUh6IPsfD0QpZdWMa15Gt8ffBrvj74NWHlwuhfsz8PVXmoWFdeR6REcCruFAoKbSq0sXc44i6q+1RnesfpjG08li8PfsnKiytZen4pyy8sp1/1frwU+hJVvGXEhJWiQM+eaUyd6snKla707m3baiNplRMFcTPtJgtOLwDg+UbPZ7utZbmWAOyN2ItZNdukcnbIkBQGDEilUqXS8b2aqyTT+++/n6+DL1y4MF+PE/ZnTTLJTAAhhMifYK9bSSZVVW3e/rLm0hpe2vISiYZEfF18+a7Dd3So1MGm53A0ZV3L0rNKT5aeX8qck3P4vO3n9g4pmx3Xd/DB7g+yZrZU9KjIG83f4JGqj0j7UwEoikKTgCY0CWjCe2HvsfrSav448wfbwrex68Yudt3YxVs73+LhKg8zoNYAmgU2K3Z/31vDLVVMjfwb4af3s3M04l5q+9Xmp84/cezmMb488CVrL69l8dnF/HXuL/rX7M+40HFU8qxk7zAdQs+e6Uyd6snGjS6kpYGrDcczSaucKIhfTvxCuimdhmUb0rp862y3NSjbAHcnd+Iz4jkZe5J6ZeoV+HxBQY49m8rWcpVkevfddws7DuFgzidYVkRLJZMQQuSP9UNGUmYScRlxNvvgmGnO5NN9n/LDkR8AaBLQhB86/UAFjwo2Ob6jG1JnCEvPL+Wvc3/xVou38HL2sndInIs/x0d7PmLdlXUAeDp5MjZ0LEPrDUWvK7zNSqWRq86VvtX70rd6X8KTw1l0ZhGLzi7iUuIlFpxZwIIzC6jiVYX+NfvzWI3HKO9R3t4h54q0yhU/9cvU55euv3A4+jBfHviSjVc3Mv/0fBafXczAWgMZ23hssfn+KywNG2ZSoYKR8HAdW7bo6d7dNgkhaZUTBZGamcovx38BLFVM/70oodPoaBbYjM3XNrP7xm6bJJlKm1wlmaTtrXQxq+aswd+SZBJCiPxx1bkS5BZERGoEV5Ku2CTJdCPlBs9veJ69kZY158PrD+fN5m/irHUu8LGLi5ZBLanpU5Mz8Wf48+yfPFPvGbvFcjPtJl8d/Iq5J+diUk1oFS1D6gzhlSavUMa1jN3iKi0qeFTgpSYvMS50HHsi9rDwzEL+ufAPFxMv8tn+z/h8/+e0q9COAbUG0C24m8Mm/MyqOauSqX3F9naORuRVY//GzO0+l32R+/jywJdsC9/G3JNzWXh6IYPrDOaFxi8Q6BZo7zDtQlEsA8BnzvRg5UrbJZmkVU4UxPzT84nPiCfEK4SeIT3vep+wcmFsvraZXTd2Maz+sCKOsPgrcINheno6165d49q1a6SnS7liSXAj5QbppnR0ik7KfYUQogAqe/7/8G8bzGXaem0rXf/syt7IvXg6efJj5x95P+z9UpVgAkvr1JA6QwDLAHBVtf3GovtJN6Yz/d/ptF7YmtknZmNSTXQN7srGxzYyqfUkSTAVMUVRaFmuJV+3/5rDgw/zVbuvaBnUEhWVLeFbeH7j8zSZ14TXt7/O4ejDdvmeuZcTN09wM/0m7k7uNAloYu9wRD41C2zGgp4LWPzQYloGtcRgNvDz8Z9ptaAV7+9+n5i0GHuHaBe9elk+H65bp8dgsM0xra1yDz0knz1F3mSaM5lxdAYAzzV4LscZlta5TLtv7Maslq5WN1vIVSXT3Zw7d4558+Zx6tQpzGbLX7xGo6F27doMHjyYatVklk9xZZ3HFOwVjE6T728RIYQo9YK9gtkbubdASSaT2cSUQ1P46uBXqKjU9avLj51/LNUDZh+r+Rgf7/uY03Gn2Re5j+ZBzYvkvKqqsvzCcibtncS15GuApWXmnZbv3DHTQdiHu5M7A2oNYECtAVxMuMiis4tYdGYR11OuM+fkHOacnEMt31r0r9mfR6s/ir+bv71DZsu1LQC0Kteq1CWNS6KwcmEsfmgx269v54v9X3Ag6gA/Hv2R307+xtB6Q3mu4XOlau5W06YG/P1NREdr2bnThQcfzCjQ8aRVThTE8gvLCU8Op6xrWR6v+XiO92vk3whXnStxGXGciTtDbb/aRRhl8ZevSqazZ8/y7rvvcuHCBTp27MjTTz/N008/TceOHbl48SLvvvsu587JOs/iSjbLCSGEbdw+/Ds/YtJiGLx6MF8e/BIVlUG1B7Gs97JSnWAC8HL2ok+1PgDMOTGnSM4ZnRrNsHXDGL1xNNeSrxHkHsQ37b9hVd9VkmByUFW8qzDxgYnsHrib+T3m06daH/RaPafjTvPhng954PcH+PPcn/YOky3hliSTtMqVHIqi0LZCW5Y+spS53efSqGwjUo2pTP13KmELwvhi/xckZCTYO8wiodWS1Sa3cmXBW1Zvb5WrUkVa5UTuqarK9H+nAzC03lBcdTlPonfSOPFA4AOApZpJ5E2+kkwLFizAz8+PKVOmMGLECHr27EnPnj0ZMWIE33zzDb6+vsyfP9/WsYoiIkkmIYSwjawkU1Lek0x7I/bS7c9ubA3fiqvOlSkPTuHztp/f801RafJUnacAWHFxBTfTbhbquZadX0aHxR1Yc3kNThonxjcZz/b+23m85uM2WW0sCpdWo6VdxXZM6ziNg4MO8mmbT2lUthFG1cg7O98h0ZBot9hSM1PZF7EPgLYV2totDlE4FEWhY6WOrOizgl+6/kJdv7okZybzzaFvaLmgJT8c+cHh2jcLQ8+eliTT6tV6TAXMC0mrnMivzdc2czL2JG46N56u+/R9798yyNIyt+vGrsIOrcTJdyVTly5d8PHxueM2Hx8fOnfuzNmzZwsam7ATa5Kpmo+0PAohREHkZyaTqqr8cOQHHvvnMSJSI6juU50VvVfwWI3HCivMYqmhf0Ma+zfGYDaw4PSCQjlHbHosozaMYvTG0cRlxFGvTD1W9lnJK01fkWRfMeXt4s2QOkNY1nsZ1byrEZcRl7Wp0R52R+zGYDZQwaMC1bzlfVdJpSgKXYO7sqbfGn7s/CO1fGuRaEjkwz0fsvLSSnuHV+jCwjLw8TFz86aWvXvz3xIqrXKiIKb9Ow2AQbUH4ePic9/7h5ULAyzP06UhGWxL+UoyKYqC6R5paLPZfMcqQFF8SCWTEELYRohXCGBZqJBhuv8civiMeIatG8aHez7EpJroU60PK/uspJZfrUKOtHiyVjP9duo3mw/mXHt5LR0Xd2T5heVoFS0vN3mZf3r/Q90ysnG3JNBpdPyv2f8A+PHoj0SlRtkljq3X/n+rXIX28t65FNAoGnpV6cW6fusYUX8EAJ/s/YRMc6adIytcTk7QpYul8mjVqvy3zEmrnMivQ1GH2HVjFzpFx4gGI3L1mMYBjdFr9cSkxXA+4XwhR1iy5CvJVKtWLdasWUN0dPQdt8XExLB27Vpq15bhWMWRwWTgStIVQJJMQghRUGX0ZXDTuaGicjXp6j3veyT6CD3+6sGay2tw1jjzSetPmNphKu5O7kUUbfHzSLVH8Hb25krSlazhyQWVkJHAuM3jeHbts0SnRVPTpybLey/n1aavylDmEqZHSA9CA0JJM6bxzaFv7BLD1nBLkqldxXZ2Ob+wD61Gy6tNX6WMvgwXEy/y+6nf7R1SoevZ01J5tHKlK+Z8XhOQVjmRX9OPWGYx9anehwoeFXL1GBetC6EBoYC0zOVVvpJMTzzxBKmpqbz00ktMmTKFP/74gz/++INvvvmGl156idTUVJ544glbxyqKwJWkK5hVM+5O7gS4Btg7HCFEYVFVtOnXQMp/C5WiKFlzmawJ/P9SVZVfT/xK72W9uZJ0hcqelVn6yFKeqvuUVDbch6vOlcdqWtoI55ws+ADwzVc303FJRxafXYyCwvMNn2dV31U08m9U4GMLx6MoCm80ewOAeSfncTHhYpGe/0bKDU7HnUZBoU35NkV6bmF/Hs4evNzkZQC+Pvg1KZkpdo6ocLVrl4G7u5kbN7T8+69Tnh8vrXIivy4kXGDVxVUAjG44Ok+PzWqZk+HfeZLrJJP5tpRzlSpVmDRpEo0bN2b//v0sWbKEJUuWcODAARo3bsykSZMICQkpjHhFIbu9VU4+3AhRQqkqPifHEri7BfqoZfaOpsQL9sx5w1xKZgovbHqBN3a8gcFsoFtwN1b1XUVD/4ZFHWaxZW2ZW39lPeHJ4fk6RrIhmYnbJjJo9SAiUiKo4lWFvx75izdbvIleV/BtSMJxtSrfio6VOmJUjXy+//MiPbe1iqmxf2N89b5Fem7hGAbVHkSIVwjRadHMODLD3uEUKr0eOnWytI3nZ8uctMqJ/PrhyA+oqHSq1InafnnrtmpZzjL8e/cNmcuUF7lOMo0aNYrZs2dz7tw5ACpVqsSECRP49ddfmTFjBjNmzODXX3/l1VdfpWLFioUWsChcMo9JiJLP4/I3uEVZ1na7RS6yczQlX9aGuf8kmU7Hnqbn3z35+/zfaBUtb7d4m1ldZuVqGKW4pbpPdVqVa4VZNeer5WTn9Z10XtKZeafmATCs3jDWPbqOZoHNbB2qcFD/a/Y/FBSWXVjGkegjRXZe6zwmaZUrvZy1zrz2wGsAfH/ke6JT7xxFUpL06HGrZS6vn9elVU7kR1RqFIvPLgZgTKMxeX58k4AmOGuciUiN4FLiJRtHV3LlOskUGBjIqlWrePPNNxk3bhyLFy8mIiICjUaDj48PPj4+aDSyxre4kySTECWbPuofvC5Nzvq9S9x2FGOSHSMq+Sp7WTbM3d4ut/jsYnot7cW5+HMEuQWx+KHFjGo4SipI82lInSEAzD89P9cDdNOMabyz8x0eX/E4V5OvUsmjEot6LeKDVh/I5rhSpl6ZevSt3heAT/Z9UiTnNKvmrEqm9hXaF8k5hWN6uOrDNPZvTKoxla8OfmXvcApVp04ZuLioXLqk4+RJXa4fJ61yIr9mHZ9FhimDpgFNaR7UPM+Pd9W5Zs1lkpa53Mt1VujDDz9k6tSpDBw4ECcnJxYtWsS4ceN44403WLVqFQkJCYUZpygi1iSTrNEVouRxSjqCz6lxACRXHEGmW3UUNROX2I12jqxkC/EMASyVTGnGNCZsncC4zeNIM6bRtkJb1vRbk683PuKW7iHd8Xf1JzI1kjWX1tz3/vsi99FlSRdmHZ8FWFpW1j+6nlblWxV2qMJBTWg6ASeNE1vDt2YlfwrT8ZvHiU2Pxd3JnSaBTQr9fMJxKYrCWy3eAmDeqXmcjy+5W6zc3VXat7dumct9Mn/1ammVE3mXbEhmzgnLvMbnGz2f7wt51pa5nTd22iy2ki5PpUf+/v707duXyZMn88UXX9C7d28SExOZPXs2o0aN4uOPP2br1q2kp0sZY3FlfWGTSiYhShZNRiR+R59FY04n3a8jidXeJr1sdwD0Mff/UC7yz1rJdCnxEr2X9eb307+joDC+yXjmdZ9HWdeydo6w+HPWOvNELcvCkXsNAE83pjNpzyT6Le/HxcSLBLkH8Vv33/i87ed4OHsUVbjCAVX2qpw13+uTvZ9gVvO5/iqXrNsQW5dvjZMm70OQRckSVi6MTpU6YVJNfLr/U3uHU6h69rR8TszLXKbly6VVTuTdb6d+I9GQSDXvanQN7prv48hcprzLd39b5cqVefLJJ5k6dSrvv/8+nTp14uLFi0ybNo0RI0YwZcoUW8YpikCSIYmotCgAqnhXsXM0QgibMaXhd2woWkMEmW41iKs7DRQt6WW7AaC/uQHMGXYOsuSq6FERjaIh3ZTO8ZvH8dP78XuP33ml6StoNVp7h1diDKo9CAWFHdd3cC7+3B23H4k+Qo+/ejD9yHTMqpnHajzGxkc30qFSBztEKxzR2NCxuDu5cyTmCP9c+KdQz2VNMkmrnLB6o/kbaBQNKy+u5EDkAXuHU2i6dElHp1M5dcqJ8+fv/xoorXIiPwwmAz8d+wmwbJTTKPkf6/NAwAPoFB3XU65zNemqrUIs0WwyRKl27doMHz6cyZMn88ADD2AwGNi5U8rJihvr6l5/V3+8nL3sHI0QwiZUFZ/T43FOOoxZ50Nsg9moOsvPd6ZnY0zOgWhMybjE77JzoCWXs9aZEK8QAJoFNmNtv7Uy6LcQVPSsSKfKnQD47eRvWX9uMBmYfGAyDy19iDPxZ/B39efnLj8z5cEpeLt42ytc4YDKupZlVINRAHy2/7Ncz/fKq9TMVPZF7gNk6Le4pbZfbR6v8TgAk/ZOKrEVEz4+Kq1bWy5s5aZlTlrlRH78df4vIlIiCHQLpF+NfgU6lpuTG438GwGwK0LeL+dGgZNMBoOBHTt28Nlnn/H888+zf/9+/Pz8ePjhh20RnyhCMvRbiJLH4/IU3KKWoio6Yuv9hMk15NaNiob0spbyYX3MavsEWEp81+E7vmj7BYseWkQ593L2DqfEsrY7LTq7iDRjGidjT/LQ0of4+uDXmFQTj1R9hI2PbaRbSDc7Ryoc1cgGIymjL8OlxEvMPzW/UM6x68YuMs2ZVPKoRBUvqRwXt7za9FX0Wj17Ivaw7so6e4dTaKwtc6tW3b9lTlrlRF6ZVTPf//s9AMPrD8dF61LgY4aVCwNk+Hdu5SvJZDabOXjwIN9++y0jRozg22+/5dSpU7Rt25Z33nmH6dOnM3jwYFvHKgqZJJmEKFn00SvwuvQFAAk1PsHge2uosdvs2QQ2aYLxquUDjj5mLRTyDJLSrLF/Y56s/aTMXilkD1Z8kEoelYjPiOe59c/R468eHL95HF8XX77v+D3fd/oeP72fvcMUDszD2YOXm7wMwNcHvyY1M9Xm59gSbmmVa1exnWyUFNmU9yjP8PrDAfh478cYzUY7R1Q4unVLR1FUDh92Jjw855Y5aZUT+bH+ynrOxp/F08mTwXVsk5O4fS6TuL88JZlOnTrFzJkzGTlyJJ999hl79uyhQYMGvPLKK/z000+MGjWKevXqyQtmMSVJJiFKDqeko/icHAtAcsXhpJZ/Mus2ly1b8H77bbSRkTj9eRiz1gOtIRKnpMN2ilYI29BqtAyqMwiADVc3kGnOpGtwVzY+tpFHqj1i5+hEcTGo9iCCPYOJSovKmulhS1uvWbbXta8o85jEnZ5v9Dw+Lj6cjT/LH2f+sHc4hcLf30yLFgbg3gPAra1y9eplSqucyLXp/04HYEidITYbAdMssBlaRcuVpCuEJ4fb5JglWa6TTGPGjOHdd99l3bp1VKpUiZEjR/Ljjz/y6quv0qJFC3Q6XWHGKYqANclUzbuanSMRQhSEZZPcM/+/Sa4DiVXfzrpNe+UKvs8/j2K2VC3pN20hw+tBy//LljlRAjxR6wnK6Mvg5ezFN+2/4ecuPxPgFmDvsEQx4qx1ZsIDEwD4/t/viU2Ptdmxw5PDORt/Fo2ioXX51jY7rig5vF28GRc6DoAvD3xJmrFkVvD06HH/ljlrq9zDD5fMvwNhe/si9rEvch/OGmeGNxhus+N6OHvQoGwDQKqZciPXSSZXV1eefPJJpk+fzrvvvkunTp1wd3cvzNhEEVJVVSqZhCgJTGn4HRt22ya56aCxXARQ0tLwGz4cTXw8hsaNMfn5oUlIIPNadUDmMomSoaxrWbY8voWDgw7yeM3Hpbpa5Evvar2pV6YeSZlJfHf4O5sdd1v4NgAa+TfCx8XHZscVJcvTdZ+mkkclIlIj+Omo7avpHEGPHpbE0d69zkRF3fmRVFrlRH5MP2KpYnqsxmMEugXa9NjSMpd7uU4yTZ48md69e1OmTJnCjEfYicFsYECtAXSs1JHKXpXtHY4QIj+yNskdumOTHKqK98SJOB0/jqlMGWJ//JGMzp0B0OyJQ1WccEo9hy7lztXvQhQ3vnpfXHX331okRE40ioY3mr0BwOzjs7mWdM0mx91yzTKPqX0FaZUTOXPRujCx2UQApv07jZtpN+0cke1VqGCmcWMDqqqwZs2d1UzSKify6mzcWdZeXouCwnMNn7P58VsGWZJMu27Ihrn7KfB2OVEyuGhdeD/sfeZ2n2uTCfxCiKJ3r01y7j//jNuff6JqtcTNmIG5QgXSu3QBQL9hCxnelqHg+pvSMieEEGCZmdSqXCsMZgOTD0wu8PFMZhNbw2Uek8idPtX6UL9MfZIzk5lyaIq9wykU1i1zd5vL9M8/0ion8ub7I5aNct1DulPdp7rNj988qDkaRcPFxItEpkba/PgliSSZhBCiBMi+Se7jbJvknHfvxuuDDwBIfPttDGGWNawZ7dujOjuju3QJQ3ITy3GkZU4IIQBQFIU3mluqmRafXcyp2FMFOt6xm8eIz4jHw8mD0IBQW4QoSjCNouHNFm8CMOfkHC4nXrZzRLZnbZnbudOFuLhbrc2xsRq2b5dWOZF7N1Ju8Oe5PwEY3XB0oZzD28WbemXqAdIydz+SZBJCiGJOl3QMn5OWIaHJFYaRWn5Q1m2aGzfwfe45FKOR1L59SRl+awii6u5ORps2ACgHLGuSnRMPosmQqzNCCAEQGhBKryq9UFH5dN+nBTqWtYqpdfnWOGmcbBGeKOHaVWhH+wrtyTRn8tn+z+wdjs1VrWqiTp1MjEaFdetuVTNJq5zIq5nHZpJpzqRlUEuaBjYttPNYW+Z2Xt9ZaOcoCSTJJIQQxZgmI5Iyx55BY04j3fdBEqu9c+vGjAz8RoxAGxNDZp06JHzxBfxnCHL6/89lct60G4On5cq6/ubaogpfCCEc3msPvIZW0bLuyjr2RuzN93Gs85jaVWxnq9BEKfBGizdQUFh6fin/Rv9r73Bs7m5b5qytclLFJHIjISOB307+BsDoRoVTxWQVVs7SDbA7QiqZ7kWSTEIIUVxZN8ll3LBskqv3fdYmOQDvd97B+dAhzN7exM6ahep65yBk61wm5/37ydBaPvjoY2QukxBCWFXzqcbAWgMBmLR3Eqqq5vkYKZkp7I/cD8jQb5E39cvUp2/1vgB8tOejfH3/ObKePS2JpC1b9CQnK9IqJ/Js7sm5JGcmU9u3Np0qdSrUczUPao6Cwrn4c0SnRhfquYqzXCWZYmJi8vVLCCFEIVFVfE6/evdNcoDb/Pm4//YbqqIQN20apuDgux7GXL48hgYNUFQV9aglCeUStx3FmFQkX4YQQhQHrzR5Bb1Wz/7I/ay7si7Pj991YxeZ5kwqe1YmxCvE9gGKEu21B17DWePMzhs72XRtk73DsanatY2EhBjJyFDYuNElW6tc1arSKifuLd2YzsxjMwEY1XAUyn8q9m3NV+9Lbb/agFQz3Yvu/neBMWPG5OvgCxcuzNfjhBBC3JvHlW9xi/r7rpvknA4dwvsNy7DapAkTyOjQ4Z7HyujSBeejR3HaeoTM4dVwSjuPS+xG0gN6F+aXIIQQxUaQexDDGwxn6uGpfLL3EzpV6oRWo83147des8xjalehXaF/CBIlT0XPijxb71lmHJ3Bx3s/pn2F9nn6/nNkigK9eqUxbZonK1e6kpho+fmQKiaRG0vOLSE6LZry7uXpU71PkZwzrFwYJ2NPsvvGbh6u+nCRnLO4yVWSafTo7L2NqqqycuVKYmJiaNOmDeXLlwcgPDycHTt24O/vT48ePWwfrRBCCPTRK/G6+DkACTUmZdskp4mJwW/ECBSDgbTu3Ul+8cX7Hi+9a1c8v/oKl82bSXl5CE5p59HHrJEkkxBC3Ob5hs/z28nfOBN/hsVnFzOg1oBcP3ZLuGUeU/uK0ion8ufFxi+y4PQCTsaeZMm5JfSv2d/eIdlMjx7pTJvmyfr1LhgMkmQSuWMym/j+3+8BGNlgZJEtVGhZriU/H/9ZNszdQ67a5R588MFsv+Li4sjMzOTbb79l2LBh9OjRgx49ejB8+HCmTJlCRkYG8fHxhRy6EEKUPpZNcmMB6ya5wbduNBrxHTUK7Y0bZFarRvw334Dm/k/zmfXrYwoKQpOaivl8EAD6mxvAnFEYX4IQQhRL3i7evNjYkriffGAy6cb0XD0uPDmcc/Hn0CgaWpdvXZghihLMV+/LC41fAOCL/V+QZiw5SZjGjTMpV85EWppGWuVErq25vIaLiRfxcfHhydpPFtl5rRvmTsWdIjY9tsjOW5zka/D3unXr6Ny5M56ennfc5uXlRadOnVi7VrYTCSGELd1zkxzg9dFHuOzahdndnbhZs1Dv8hx9V4qSNQBct/MCJucANKZkXOJ32fgrEEKI4u2Zus9Qzr0c11OuM/vE7Fw9xtoqF+ofireLdyFGJ0q6Z+s9m/X998vxX+wdjs0oyq0B4CBVTOL+VFVl+r/TAXi67tO4O7kX2bnLuJahpk9NAPbc2FNk5y1O8pVkSkpKIiMj5yvcBoOB5OTkfAclhBDiP0zpt22Sq37HJjnXv/7C46efAIifMgVjjRp5Onx6164A6NeuI93PknDSx6y2UfBCCFEy6HV6JjSdAMB3h78jISPhvo+RVjlhK646VyY+MBGAqYenEpceZ+eIbKdnz1uVgZJkEvezO2I3h6IPodfqGVpvaJGfv2U5SzXTrgi5IHs3+Uoy1ahRg5UrV3LhwoU7bjt//jwrV66kevXqBQ5OCCEEd26Sq599k5zuxAm8X30VgKQXXyQ9HzPxMlq1wuzmhjYigsxoy9YMfcw6UM22+RqEEKKEeKzGY9T0qUl8RjzTj0y/531NZhPbwrcB0K5iu6IIT5Rwj1Z/lDp+dUgwJPDd4e/sHY7NNGtmYMiQFF58MUla5cR9WauY+tfsT1nXskV+/rByYQAylykHiqqqal4fdO3aNd577z2SkpKoWbMmQUGWGR4RERGcOXMGDw8P3nvvPSpVqmTzgB1ddHQ0mZmZ9g5DCFGCeFz+Fq+Ln6EqOm42/B2D762ZHkpcHP69eqG7fJn0Bx8kds4c0OZv44zv8OG4rlpF4ivj8Gg+C40pmegm/5DpFWqrL0UIIUqENZfWMHTdUPRaPTsG7CDIPeiu9zscfZhef/fC08mTY08dQ6fJ1c4dIe5p49WNDFk9BGeNM9v6b6OiZ0V7hyREkTlx8wRd/uyCRtGwrf82QrxCijyGqNQoQueFoqBw7Klj+Lj4FHkM9uDk5IS/v/9975evSqaKFSsyefJkevToQVJSEjt37mTnzp0kJSXRs2dPvvzyy1KZYBJCCFuzbJL7DICEGh9lSzBhMuH74ovoLl/GWLkycVOn5jvBBGTNZdKv20iGX0fL/0vLnBBC3KFrcFceCHyAdFM6Xx/8Osf7bblmaZVrXb61JJiEzXSo2IFW5VphMBv4fP/n9g5HiCL1/RHLRrleVXrZJcEEEOAWQDXvaqio7I3Ya5cYHFm+X+18fHx45plnbBiKEEKI2925SW5Itts9v/wS/aZNmPV6YmfORPX1LdD5Mjp3RlUUnI8eJc40EFeWoY9ZQ1LV1wt0XCGEKGkUReGNZm/Q759+zD89n5ENRlLNp9od97MO/ZZWOWFLiqLwVou36Pl3T/489ycjG46kfpn69g5LiEJ3LekaS88vBeD5hs/bNZaW5VpyPuE8u27somtwV7vG4mjyVcl0u7i4OC5dukR6eu7WuAohhLg/TUbUPTfJ6VevxnPKFAASvvgCY716BT6nuUwZMps2tZz/oAFVccIp9Sza1HMFPrYQQpQ0Lcq1oHPlzphUE5/t/+yO25MNyeyP3A/I0G9he438G9G7Wm9UVD7e87G9wxGiSHyy7xNMqok25dvQ0L+hXWORuUw5y3eSad++fbz00kuMGjWK1157jXPnLB9CEhMTmThxInv3StmYEELkiykdv2NDLZvkXKsRV3d6tk1yunPn8Bk3DoDkYcNI69fPZqe2bplz2bCNDB9La55rzFqbHV8IIUqS/zX7HwoKKy6u4HD04Wy37byxE6NqJNgz2G4tHaJke+2B13DSOLElfAtbw7faOxwhCtX6K+v5+/zfaBQNbzZ/097hZG2YO3bzGImGRDtH41jylWTav38/kydPxtPTk8cffzzbbV5eXvj5+bF582ZbxCeEEKXLfzfJNZiN6uSddbOSlITvsGFokpPJCAsj8e23bXr6rCTT9u2kuz0IyFwmIYTISR2/Ojxa41EAPt77Mbfv05FWOVHYgr2CearOUwBM2jMJs2yEFSVUsiGZ17dbxjeMbDDS7lVMAOXcyxHiFYJZNbMvYp+9w3Eo+UoyLVmyhLp16/Lhhx/SrVu3O26vWbMmFy9eLHBwQghR2nhc+Q63qL9QFR2x9X7E5Fb11o2qis/LL+N07hymoCDivv8enJxsen5j9eoYQ0JQDAbUk+4AOCUeRJMRadPzCCFESTGh6QScNc7suL4jWzXJlnDL0O/2FaRVThSecaHj8HDy4NjNY1mzaoQoaT7f/znXU65T2bMyrzZ91d7hZGkZZKlmkpa57PKVZLpy5QphYWE53u7t7U1iopSMCSFEXuijV+W8SQ7wmDoV11WrUJ2dif3pJ8y5WCGaZ4qStWXOZdM+DJ6hKKjob66z/bmEEKIEqOhZkafrPg3ApL2WapKrSVe5kHABraKlVflWdo5QlGRlXMvwfCPLAOTP9n1Ghimj0M6VZEhix/UdnI8/X2jnEOK/DkQe4OfjPwPwWZvPcNW52jmiW6wtc7sidtk5EseSrySTi4vLPQd9R0ZG4uHhke+ghBCitLFsknsRgOQKQ+/YJOeyeTOen/1/Auqjj8hs0qTQYslqmVu/nnRfy//rY9YU2vmEEKK4Gxs6Fk8nT47fPM6y88uyKppCA0LxdvG+z6OFKJiRDUYS5BbE1eSr/HriV5sc02Q2cTL2JL+f+p1Xt75Kx8UdqfNrHfqv6E/Pv3sSnxFvk/MIcS8Gk4EJ2yagovJ4jccdrv3YOvz7SPQRUjJT7ByN48hXkqlevXps2bIFk8l0x23x8fFs2LCBRo0aFTg4IYQoDbJvkmtPYrV3s92uvXIF3zFjUFSVlEGDSB00qFDjMTRrhtnbG21sLKZrlQBwiduOYkwq1PMKIURx5af3Y3Sj0YClrWPDlQ2AtMqJouGqc2V80/EATDk0hYSMhDwfIzo1mrWX1/LJvk94/J/HqTOnDp2XdGbCtgnMPz2f03GnUVHRKlqSM5NZf2W9rb8MIe4w7d9pnI47TRl9Gd5p+c79H1DEKnpWpJJHJUyqKWubqMhnkumJJ54gNjaW119/nXXrLC0Uhw8fZsGCBYwfb3mCe+yxx2wXpRBClFR3bJL7PtsmOSUtDb9hw9DEx2MIDSXhww8LPyYnJ9I7dgRAt/0kma7VUFQDLrGbCv/cQghRTI2oP4IA1wAuJ11mzWVL9aejXXUXJVf/mv2p4VOD+Ix4pv87/Z73zTBlcCDyAD8d/YnnNz5Py/ktaTyvMc+ufZaph6ey88ZOUjJTcNO50apcK15o9AI/d/mZQ4MO8WJjS9X1qouriuLLEqXYufhzfHvoWwA+CPsAP72fnSO6u6yWuRvSMmelqLevwciDq1evMnv2bI4dO5btz+vWrcuwYcOoWLGiTQIsbqKjo8nMzLR3GEKI4kBV8Tk5FreoPzHrfIhusvzOQd9jx+L255+YypYletUqzOXLF0lo+qVL8Xv+eTJr1CB9Vlc8r04jNaAP8XWnFcn5hRCiOPr1xK+8seMNALycvTg65Ci62y4cCFGY1l5ey7Nrn0Wv1bOt/zbKe5RHVVWuJF3hYNRBDkYd5FDUIY7dPEamOfvnFQWFmr41CfUPpUlgE0L9Q6nlWwutRpvtfsdvHqfrn13Ra/UcHXIUNye3ovwS8+1w9GFSMlNoGtAUvU5v73DEfZhVM48uf5S9kXvpWKkjc7rNQVEUe4d1VwtPL+SVra/QLLAZfz/yt/Tn6rAAAMhnSURBVL3DKVROTk7452ImbL5f9SpVqsTbb79NcnIyERERqKpKYGAgXl5e+T2kEEKUKh5XpuIW9ef/b5KbkT3BBLjPmoXbn3+iarXE/fBDkSWYADI6dEDV6XA6e5bEFEuFqv7mBjAbQONcZHEIIURx8mTtJ/nx6I9cSrxEm/JtJMEkilSXyl1oHticvZF7eWHTC3g6e3Iw6iCx6bF33LeMvgyhAaE0CWhCk4AmNPJvhJfz/T/H1fWrS7BnMJeTLrPx6kYeqvpQYXwpNnUu/hyPLH0Ek2pCr9XTIqgFbSu0pW3FttT1q4tGyVdzjyhEv538jb2Re3HTufFpm08dNsEEtyqZDkcfJs2Y5lCDye2lwK98Hh4eVK9e3RaxCCFEqWHZJPcpAAnVP8Tg2ybb7c67duH1wQcAJL79NoZ7bPQsDKqXF4aWLXHZvh3d7uuY6gegNUThEr+LDD+ZMSKEEHfjpHHii7Zf8MGeDxjRYIS9wxGljKIovNniTXov682eiD1Zf+6kcaJ+mfpZCaXQgFAqe1bO1wd3RVHoUaUHPxz5gVWXVhWLJNPS80sxqSY0ioZ0UzpbwrewJXwL7LUk29pUaEO7Cu1oW6EtFTwq2DvcUu9Gyg0m7Z0EwOvNXnf4f5PKnpUp516OGyk3OBB5gDYV2tz/QSVcrpJMW7ZsAaBdu3YoipL1+/tp314+iAghxH9l3yT3LKkVnsp2u+b6dXxHjUIxmUjt25eU4cPtESbpXbvisn07+rXrSG/fBfcb89DHrJYkkxBC3EOr8q1Y3Xe1vcMQpdQDgQ/wXsv3OBJzhIZlG9IkoAn1ytSzaYtYjxBLkmn9lfVkmDJw0brY7Ni2pqoqyy4sA+Crdl/RsGxDtoZvZVv4Nnbd2MXN9JssPb+UpeeXAlDVuyptK7SlXYV2tCrfKlfVXcJ2VFXlzR1vkpyZTJOAJjxd92l7h3RfiqIQVi6MP8/9ya4buyTJRC5nMg0YMACAefPmodPpsn5/PwsXLixYdMWQzGQSQtyLJiOKsgd7ocu4Trpve2IbzMk26JuMDMo++ijOhw6RWbcuMcuWobrap+xWe+UKgWFhlna9LdPwuzwKk3MQkWH7QErLhRBCiFLJrJpp9nszIlIjmNNtDp0qd7J3SDmyzpBy0brw7+B/8XT2zLrNYDJwKOpQVtLpcPRhTOqt7ekaRUNj/8ZZVU5NAprgrJWRAYVpxcUVjFw/EieNE6v7rqa2X217h5Qr807NY+K2ibQMasmSh5fYO5xCY9OZTFOnTrXcWafL9nshhBB5YErH7/gwdBnX77pJDsD77bdxPnQIs48PsTNn2i3BBGCqXJnM2rVxOnUK5WAa5gB3tIYInJL+JdMr1G5xCSGEEMJ+NIqG7iHdmX1iNisvrnToJJO1iqlDxQ7ZEkwAzlpnWpRrQYtyLZjwwAQSDYnsvL6TbeHb2Bq+lQsJF7IGpn9z6BvcdG6ElQvLqnSq6VvToWcFFTfxGfG8teMtAMY0GlNsEkwALYMsc5kORR8i3Zhe6ofL5yrJ9N9sVW6yV0IIIW6jqvicmYBz4kHMOh9iG8xGdfLOdhe333/Hfd48VEUhbupUTMHBdgr2lvQuXXA6dQr9+k1kvNAR1+jl6GNWS5JJCCGEKMV6hPRg9onZrLm8hs/MnznkkHtVVVl+fjkAj1R75L7393L2ontId7qHdAcgPDk8K+G0PXw7N9NvsuHqBjZc3QBAoFsgbcq3sQwRr9CWIPegwvtiSoFJeyYRlRZFdZ/qjA0da+9w8qSqd1UCXAOISoviUPQhwsoV7SxVRyP9DkIIUQQ8rkzFLfJPVLR33STndPAg3m++CUDSxIlkdOhgjzDvkN6lCwAumzaR7t0ZAH3MGnuGJIQQQgg7a1muJb4uvsRlxGUbMu5IjsQc4XLSZfRaPZ0rd87z4yt4VGBgrYFM7zidw4MPs6bfGt5u8TbtK7RHr9UTmRrJknNLeGnLSzT9vSkdFnXgnV3vsP7KepINyYXwFZVcO6/v5PfTvwPwRdsvHHrO190oipK1ZW73jd12jsb+cpVyfv/99/N8YEVReOedd/L8OCGEKGn00atvbZKrcecmOU10NH4jRqAYDKT16EHyiy/aI8y7ygwNxVS2LNqYGMznPFEVJ5xSz6JNPYfJTTaLCiGEEKWRTqOjW3A3FpxZwKqLq2hdvrW9Q7qDtVWuc+XOuDu5F+hYGkVD/TL1qV+mPqMajiLdmM7+yP1su76Nbde2cSTmCGfiz3Am/gyzjs1Cp+hoGtg0q8qpsX9jh6z2cgRpxjQmbpsIwJA6Q2ge1NzOEeVPy3ItWXZhGbtu7OJlXrZ3OHaVq0qmXMwGt8ljhBCipNElH//PJrn/bMnIzMR39Gi0ERFkVq9O/NdfgyP192s0t6qZNu4gw6cVAK4xa+0ZlRBCCCHsrEeVHgCsurQKs2q2czTZqarK8gu5b5XLK71OT5sKbXi92eus7LuSI0OOMKPTDAbVHkSwZzBG1cieiD1MPjCZ3st6U39OfYauHcrs47M5F39OPivf5ptD33Ax8SJBbkG80fwNe4eTb63KWd4jH4g8gMFksHM09pWr7XIi92S7nBDCSmOIpuyBnv+/Sa4dsQ3m3jHo2+vdd/GYOROzhwcxK1ZgrO541UH6NWvwGzoUY6VKJC8ehc+5NzF4NSWmyTJ7hyaEEEIIO0k3ptPot0YkZyaz7JFlNA1sau+QsuyP3E/vZb1xd3Ln38H/4qor2kUqlxMvZ81z2nF9B/EZ8dluL+9ePmtrXZsKbSjrWrZI43MUx28ep8dfPTCpJn7u8jPdQrrZO6R8U1WVxvMaE5MWw98P/02zoGb2DsnmbLpdTgghRB6Z0vE7ZtkkZ3StetdNcq5//YXHzJkAxH/zjUMmmAAy2rZF1evRXb2KMbYaAE6JB9FkRGJ2CbRzdEIIIYSwB71OT6fKnVh6fimrLq1yqCSTtVWua+WuRZ5gAgj2CibYK5jBdQZjMps4dvMYW8O3si18G/si9nE95ToLzixgwZkFANT1q0u7iu1oW74tLcq1sEvMRc1kNjFh6wRMqoleVXoV6wQTWMYFtQhqwYqLK9h1Y1eJTDLlVoGSTAcOHODQoUNER0cDlq1zoaGhNG3qOE8wQghR5LI2yR3ArPPmZoPZqE4+2e6iO34c71dfBSDpxRdJ79HDDoHmjurmRkabNujXr8d58yEMrUNxTjqE/uY6UssPtnd4BaYYk3GNWorbjd/RGBOJCf0Ls3PpvKIohBBC5EXPkJ5ZSaY3m7+J4gAt/2bVzIoLK4DCaZXLK61GSyP/RjTyb8SLjV8kzZjG3oi9bA3fytZrWzkReyLr1w9HfsBZ48wDgQ/QrmI72lVoR/0y9dFqtPb+Mmxu1vFZ/BvzL97O3nzU6iN7h2MTYeXCWHFxBbtv7C52G/JsKV/tcikpKUyePJkTJ06g0Wjw9fUFIC4uDrPZTJ06dZgwYQLu7nkfsLZ69WqWL19OfHw8wcHBDB06lOo5XN3fs2cPf/31FxEREZhMJoKCgnj44Ydp165dtvusW7eOCxcukJyczOeff05ISEi247z33nucOHEi25917tyZkSNH5jl+aZcTQnhcnorXxU9Q0XKz4W8Y/Nplu12Ji8O/Z090V66Q/uCDxM6ZA1rHfvPgNm8ePhMnYggNJX1aN7wufkq6X0diG861d2j55pR0FLfrc3GN+huNKSXrz+PqTCUtsK8dIxNCCCGKh5TMFBrObUi6KZ21/dZSr0w9e4fE7hu7efSfR/Fy9uLw4MMOv6ksJi2G7eHbs9rrrqdcz3a7j4sPrcu3pm2FtrSr0I5gr2A7RWo7VxKv0HFJR9KMaXzR9guerP2kvUOyiZOxJ+m8pDNuOjdOPH0CJ42TvUOyqUJtl/vll184efIkgwYNomvXruj1egDS09NZu3Ytv//+O7/88gsvvPBCno67c+dO5syZw4gRI6hRowYrVqxg0qRJfPPNN3h7e99xfw8PD/r160f58uXR6XQcPHiQ6dOn4+XlRePGjQHIyMigdu3ahIWFMWPGjBzP3alTJwYMGJD1e2dn5zzFLoQQAPqYNXjevknuPwkmTCZ8X3gB3ZUrGIODiZs61eETTADpnS2rf50OHyZRfQsAl7jtKMYkVJ2nPUPLk6yqpeu/4Zx8JOvPja5VUTUuOKWcRJt2yX4BCiGEEMWIu5M77Su2Z83lNay6tMohkkxLzy8FoHtId4dPMAGUdS1Ln+p96FO9D6qqciHhAtvCt7EtfFvWPKcVF1ew4qKlOivYM5g2FdrQrkI7Wpdvja/e185fQd6oqsr/tv+PNGMaYeXCeKLWE/YOyWZq+dbCx8WH+Ix4jsYcpUlAE3uHZBf5SjLt27ePrl278sgj2csP9Xo9jzzyCDExMWzZsiXPx/3nn3/o1KkTHTp0AGDEiBEcPHiQTZs20adPnzvuX69e9iexnj17smXLFk6dOpWVZLJWNUVFRd3z3C4uLvj4+OQ5ZiGEsNIlH8fnxAsoqKSUf+bOTXKA5+TJ6DdvxqzXE/vTT6i+xeONgTkwEEPjxjgfPoxu5wWM1aqiS7uAS+wm0gPsX4p+P5aqpd9wjforq2pJVZxJ8+9JarlBGHzC8LjyLU4XT6JLu2znaIUQQojio0dID0uS6eIqXm36ql1jMZqNWcmYR6o6/vuT/1IUhWo+1ajmU41n6j2D0WzkcPThrKTTgcgDXE66zOVTl5l3ah4KCg3LNqRthba0rdCWZkHNHD6xtuTcEraEb8FF68LnbT93iBZLW9EoGloGtWT15dXsur5Lkkx5epBOR/ny5XO83VpZlBdGo5ELFy5kSyZpNBoaNGjAmTNn7vt4VVU5duwY169fZ9CgQXk6N8C2bdvYtm0bPj4+NG3alEcffRQXl5x/QDMzM7O1xSmKgqtryR/QJko5VcUldjOZ7v/H3n3HR1Hnfxx/zexsTy8k9I6CBSnSFLGiIDb0bFhP7Gc9++nZ9QR7/yl6FsAuKB5FEJWuUkSRJiShpfdk++7M748NgUiAkGyyKZ/n48HDye7szGdDMXnn8/18+6DbOka7mmZF9ReQ9PtVqLobX+JIyno9us85trlziX35ZQDKnn2W4BHR/2nfofCedhqWX3/FOn8+nqFnELvjdWyF3zbbkGnPrKVpWCrWVj8etPfA1WECnrS/oVuS9zxu6waAySshkxBCCFFXp3U9DU3R2Fiyka2lW+mZ0DNqtSzLWUaRt4hEayLHdzw+anVEiqZqDE4bzOC0wdwx8A5cARfLc5azaNciluxawqaSTawtXMvawrW8uvZVbCYbQ9OHckKnEzi+4/H0S+qHqqjRfhvVijxFPLL8EQDuHHgnPeJ7RLegRjCsfThkWpG7gpu5OdrlREW9QqahQ4eyYsUKRo8ejarW/EMbCoVYvnw5w4YNO6RrlpeXo+v6Pt1ECQkJZGdn1/4iwO12c/311xMMBlFVlWuuuYajjz76kO59/PHHk5KSQlJSEtu2bWPatGlkZ2dz1137T+JnzJjB559/Xv1x9+7deeaZZw7pvkK0NM6dbxG/9TF0UywlfV/Bl3JatEtqHv6yk1xxvzf32UlO27KFhNtuA6By4kQ857W8mT/e004jbvJkrIsWURkzkVhex1b0Heh+UJvPEuP9dy2Nwd3+MvwJw6GWn5qF7OEZB5oslxNCCCHqbPfMoB93/cicrDn845hDG5kSSbO2zgJgbPexrW4eDoSXJ57a5VRO7RIeY5DrymVJ9hIW7VzEkuwl5Lnz+HHXj/y4K7yqKNmWXL20bmTHkXSMie4PiR9Z8QglvhL6JfXj+qOvj2otjWV4++EA/Jz7M0E9iKY2aK+1Fqle73jkyJG8++67PPjgg5x66qmkp6cDkJOTw4IFCwgGg4wcOZKMjIwar+vRI/JJpc1mY/LkyXi9Xn7//Xc++OAD0tLS9llKdyCnVs0aAejSpQuJiYk89thj5ObmVr+3vzrvvPMYN25c9cetqc1PiNqY3FuIy5wEgBqqIHndVZR3u4vKrrdBM/oJSZMzDBI233PAneSUigoS//531MpKfMOHU/7gg9GptYGC/foR7NgRbdcu1N9chBypmAIFWEtX4Pvr7KkmpgRd2PNn1tK11B1X+8vwpNfsWqpB17GsWoX9i48hC0zX5KOE3BgmR9MUL4QQQrRwY7qPiXrIFNADzM6aDcBZPc6KSg1NLd2ZzgW9L+CC3hdgGAabSzazOHsxi3YuYnnOcoq8RXy19avqOVU94ntUB04jOowgzhLXZLUu3LGQL7d8iaqoTD5hcqsMAQH6JvUlzhJHub+cP4r+oH9q/2iX1OTqFTI98sgj1cdbt26t9ZyHH354n8c++eST/V4zLi4OVVUpLS2t8XhpaekBZyWpqlodBHXr1o1du3Yxc+bMQwqZ/mr3bnYHCpnMZjNmc+v8iyHEPowQiRv/iaJ78SWOJGjviTP7PeKynsVcuY7Sw19sUcOfIylmx+s48r7AwERxvzcJOf7SIq7rJNx+O+atWwmlp1PyxhvQUv/tUBS8o0cT89//Ypu/AO81o3HmTMNWODdqIZNWsQ5n9od/6Voy7zVraUStXUsA2oYN2GfOxD5zJtrOnXueGASm4dsIxvRtircghBBCtHindz2d+5fcz68Fv7KrcldUOmYW71pMqa+UFHtKdTdJW6IoCoclHcZhSYcx8ciJ+EN+1uSvYdGuRSzetZhfC34loyyDjLIM3lv/HibFxIgOIxjfazxju40lxhLTaLW5Ai7uW3IfABOPnMgxqcc02r2izaSaGJI+hAXbF7A8Z7mETHV14403RroONE2jR48erFu3jiFDhgCg6zrr1q3jjDPOqPN1dF2vMSupPrKysgBIbCEDeYVobM6dU7CUr0Q3xVB62HOEbB3xxx5Nwub7sBfORVs9juIj3yHk6BXtUpuUrXAesRlPA1DW+7F9d5IDYl59FfvcuRgWC8VTpqDXYdvP5sx32mnhkGnBAkrvnlQVMs2jrPcTTdbR1pCuJdOOHdi/+gr7zJmYN2yoflyPicGIicGUmwv5oHkkZBJCCCHqqp2jHUPSh/BT7k/MzZrLNUde0+Q1fL31awDO7H5mm1yi9FcWk4Wh7YcytP1Q7h58N+X+cpZnh+c5Ldq1qMYudvcvuZ8zup3B+b3P54SOJ0T88zdp5SR2Ve6ic0xn7h50d0Sv3RwNbz+cBdsXsCJnBTccfUO0y2ly9frTc+KJJ0a4jLBx48bx2muv0aNHD3r16sXs2bPx+XzV93v11VdJSkri0ksvBcJzkXr27ElaWhqBQIA1a9awePFiJk6cWH3NyspKCgsLKS4uBqie75SQkEBCQgK5ubksWbKEgQMHEhMTw/bt23n//ffp27cvXbt2bZT3KURLYnJvrV4mV97zIUJVA7897S8i6OxD0rqJmN1bSF01rk3Naaq5k9yVuDtetc851u+/J3ZS+HNX9uSTBAYMaOIqI883bBh6TAymvDz0HXHoJicmfy7mit8IxB3TuDfXA8Rsf4WYHW8eUteSWlyM7euvsc+cifWXX6ofNywWvCefjOfcc/GeeiqxL78cHsyeDyZvVuO+FyGEEKKVGdNtDD/l/sTszNlNHjL5Qj7mbZsHtMxd5ZpCnCWO07udzundTgdgW/k2ZmyZwRdbviCjLIOZW2cyc+tMUuwpnNPzHM7vdT5Hpxzd4LEwq/NX8866dwB4ZuQzOMytfxzB3nOZQnoIk2qKckVNq1lFvCNGjKC8vJxPP/2U0tJSunXrxgMPPFC9XK6wsLDGH3Kfz8eUKVMoKirCYrHQsWNHbrnlFkaMGFF9zsqVK3n99derP37xxRcBuOCCC7jwwgvRNI3ff/+9OtBKTk5m6NChjB8/vknesxDNmhEiceOd1cvk3O1r7twYiBtAweC5JP5xHdayn9vMnKbwTnJXh3eSSzi+1p3kTNu2kfiPf6AYBq4JE3BXheMtntWK78QTsX/zDbYFP+A782TsBbOwFc5t1JDJ5N5K4oZbsVT8Chy8a0lxubDNm4d9xgysixahBIMAGIqCf8QIPOedh2fMGIy9lmMHd/9goaqTSQghhBB1N6bbGB5Z8Qg/5f5EgbuAVEfTdW//uPNHyv3lpDvSGZI+pMnu25J1jevK7QNv57YBt7G2cC1f/vklM7fOpNBTyDvr3uGdde/QK6EX43uNZ3yv8XSO7XzI9/CH/Nyz+B4MDM7vdT6jOo1qhHfS/ByRfAQx5hjK/GVsKNnAkclHRrukJqUYhmHU54UFBQX8+OOP5OXl4XK5+OtlFEXhnnvuiUiRLUlBQUGDl+u1RpaSpWje7XhSxuwzFFk0X84d/1e1m1wMBcd+R8jWqfYTdT/xWx7Bmf0+AJ6UM1rvnCbdR8qvF2IpX0nQ3oOCgbP2HfTt8ZBy9tmY16/HP2AAhV98AVZrdOptBPbPPyfxttsI9OtH5dSbSdxwMwFHHwqGfB/5mxkGjpypxG15FFX3oGvxlPV+Ek+7c/ftWvL7sf74I/YZM7DNm4fq9e556uij8Zx7Lp6zz0Zv377WW1mWLSPlb3+D9uB9ZxTF/adH/v0IIYQQrdiYGWP4rfA3Jo2cxITDJxz8BRHyj4X/YMbWGUw8ciKPDt/3h3+ibgJ6gEU7F/HFli+YlzUPb2jP11JD0oZwfu/zGddjHAnWhDpd76U1LzFp5SSSbEn8+LcfSbIlNVLlzc/lcy9n4Y6FPDr8USYeOfHgL2gBzGYzqXUY/VGvTqYlS5bw2muvoes6DocDh2PfljfZbU1UC3lJ+v1KVN1D/J8P4kkdh6vDZQTiBu93IK+Ivn2Xye0nYAJQLZT1eapqTtP9e81penffQdgtmWGQsOme8Hyq/ewkh2EQf/fdmNevJ5SSQvFbb7WqgAnAe/LJGKqKef16Ap7DMBQzZvdmTO6tEf39Vv0FJGy6C1vRAgB8CcdRcvgL6La9honqOpaff8Y+Ywb2b75B3WvziGC3bnjGj8d9zjmEeh18XlhodydTAWiVWRF7H0IIIURbMbb7WH4r/I05mXOaLGTyBD18u/1bQJbKNZRZNXNKl1M4pcspVPgrmJM1hy/+/IKl2Uv5Oe9nfs77mYeWPcSpXU5lfK/xnNzlZKym2r/O3VK6hZfWvATAY8Mfa1MBE8Cw9GEs3LGQFTkrWk3IVFf1Cpk++ugjOnbsyJ133kmHDh0iXZNoZTTvdlTdA4Cie3HkfY4j73MCzsNxt5+AO228dDc1NwdZJrc/nvYXE3QettecpjNb1Zym8E5yn+9/JznA+c47OGbMwDCZKPm//0Nvhf9GGklJ+IcMwbpiBZbvV+AbOAJbyY/YCr/F1SUyG0NYC78lYdNdmAJFGIqF8h734+o0MbwM0zDQ1q/HMWMG9pkzMeXkVL8u1K4dnrPPxnPeeQT69z+kIDuUno5h1lACQUx5O0EPggwOFUIIIepsTLcx/OeX/7AkewllvjLirfGNfs/vtn+HK+CiU0wnBrYb2Oj3aytiLbFc2OdCLuxzITmuHGZumckXW75gQ/EGZmfNZnbWbBKsCYzrPo7ze5/PsWnHVjea6IbOvYvvxRfycXLnkzm357nRfTNRMKz9MABW5KxAN3TUVjxK5K/q9U7Ly8s57bTTJGASdaJ5MgHwxxxNwYCvcadfiK7aMLs2Er/lIdKXDyJhw+2Yy36B+q3eFBHm3PnOXrvJPXtI36gH4gZQMGgOvvghqKEKktZdTUzWC2DojVhx47MWfnvQneQsy5cT99hjAJT/+9/4hw1r0hqbkve0cHBomz8fb0p4gKS9cG6Dr6sEXcRvuofkdVdjChQRcPalYNBsXJ2vA0XFNns2qSefTLvRo4l54w1MOTnosbG4L7qIwo8+Im/lSsoffZTAMccceqekyUSoU3jegJIXwuTb1eD3I4QQQrQlvRJ60SehDwE9wILtC5rknl9nhHeVO6vHWbKappG0d7bnxv43suD8BcwfP58bj76RdEc6pb5Spm6cynmzzmP4x8OZtHISW0q3MH3jdFbkrsChOXj6uKfb5O/L0alH49AclPhK2FyyOdrlNKl6hUy9e/emsLAw0rWIVsrkDodMQUd3AvGDKD38BfKGr6a095MEnH2rups+I3XNuaSuPBXnzndRAqXRLboNCy+TewaowzK5/dCt7Sjq/wmuDleiYBCX9SyJf1yLEqyMdLlNQqtcT+L6mw+4k5yanU3iDTeghEK4x4/HdU3Tb93blHaHTNZly/BZw5stmMtXofry631Nc/lqUleNxpkzDQOFys43UDDofwRj+gLgeP99Eq+7DvPmzRhWK56xYyl++21yf/2V0uefx3/CCWBq2O4dMvxbCCGEaJgx3ccAMCdrTqPfyxVw8d327wBZKtdU+iX348GhD/LzJT/z8diPubDPhTjNTnZU7uClNS8x6rNRPLD0AQDuPfZeOsUe+vcSrYFZNTM4bTAAy3OWR7maplWvkOmqq65i8eLFrFixItL1iFZI82QAELJ3r37MMMfj7ngVBYPnS3dTc2KESNj4zwMuk1M8nrpdq2pOU8lhz2EoFuyFc0lZPQ6Te2uEi25c4Z3krjrgTnL4fCRddx2mwkIC/fpRNmlSq583FurZk0DPniiBAOblG/HHDkDBwFY0/9AvpgeJzXyOlNXnonmyCFo7UNT/E8p7PgSqFQyDmJdeIuGBB8K79V1xBblr1lDy9tt4x44Fmy1y76tLl/BBHpg8WRG7rhBCCNFWjO0+FoDvd3yPO+Bu1HvN3zYfb8hLt7huHJVyVKPeS9RkUk2M7DiSF0a9wNrL1vL6ya9zcueTMSkmQkaIAakDuLrf1dEuM6p2L5lrayFTvYZNdOnShYsvvpgXX3wRq9VKcnIyqlozr1IUhcmTJ0ekSNGy7Q6ZgnuFTNUUJdzdFD8Ipecj2PO+xJkzFbNrI468z3DkfbbX7KbzMcyNv667LXPufAdr+S/7XSZnWbaMpCuvJHDUUeF5Q3XYXSA8p6kPSeuuxez+s2XNadJ9JK2biObbRdDeneIj/g9U8z6nxT/0EJY1a9ATEiieMgXDbo9CsU3Pd9ppmLduDS+Z++fpWCrWYCuci7tD3Qd9mtwZJG64FUvFGgDc7c6jrPeTe/6u6zpxjz1GzNtvA1Bx++1U3HVXo4V4NTqZvNLJJIQQQhyqI5KOoEtsF7ZXbOf7nd9zZvczG+1eslSuebBrds7peQ7n9DyHQk8hS7OXMrLjSExqwzrMW7rh7YcD8FPuTxiG0Wb+jNark2nevHm88cYbmM1m0tPTiY+PJzY2tsavmJiYSNcqWiht93K52kKmvRjmeNydrqZg8AIKBny1T3dT2vKBVd1NK6W7qREcbJmcUl5Owm23obrdWH/6iZQzz0T74486XTsQNzA8pynu2JYzp6kuO8kBjmnTcE6bhqEolLz22p4dytoA7+jRANi++w5vwqkAWEuW1G1ZpGHgyJ5K6srRWCrWoJviKOn7GqX9Xt0TMAWDJPzzn9UBU9kjj1Bx992N2iVW3cmUDyZZLieEEEIcMkVRGNOtaslcZuMtmSv3l/P9ju8BWSrXnKTYUzin5zltbje52vRP7Y/NZKPQU8iW0i3RLqfJ1KuTacaMGRx22GHcd999OByOSNckWhEl5MHkzwXCM5nq9iKFQPxgSuMHS3dTU6nDMrn4f/8bLTs73OlhMqFlZJBy7rmUvvIK3jPOOOgtdGs7io75lPgtD+PM/oC4rGcxV66j9PCXMLTmF0rH7Hijeie5kn5vEnL02ucc8+rVxD/4IAAV996L78QTm7jK6PIPGkQoMRFTSQnq+lKC9h5ongysxd/jbXfWfl+n+gtJ2HRX9dI6X8IISg5/Ed3Wcc9JXi+JN9+Mfe5cDJOJ0mefxXPhhY39lgjuDpkKQJPlckIIIUS9jOk+hv/7/f9YsH0BvpBvv9vcN8S8rHn4dT+9EnrRN6lvxK8vRENZTVYGpQ1iafZSlucsp3di72iX1CTq1cnkdrs5/vjjJWASB2Wq2llO1xIwzIeeZtepu2njHdLd1EB7lsk5a10mZ5szB8dnn2GoKqUvvUTBrFn4Ro5EdbtJnDiRmFdeqdvnX7VQ1udpSg97tlnPaQrvJPcUEN5JzlfLTnJqQQFJ116L4vfjGTOGyn/8o6nLjD5Nw3fyyQDYFiyo3mXOVjhvvy+xFs4n9ZdTsBXNx1AslPV8iKL+n9QImJTKSpKvuCIcMFmtlLz9dpMETLBXJ1M5mIqz5N8VIYQQoh4GtRtEmiONikAFS7OXNso9di+VO7vH2W1mGZJoeXYvmVuR23bmWdcrZOrXrx/bt2+PdC2iFdI8dVsqd1C7u5t270zX6wkCzsNRdS+O3E9JXXMOqStPw7HzvyiBsghU3naY3BkHXCanFhYSf++9AFTedBP+Y4/FSEigaOpUKq++GsUwiPvPf0i49Vbweut0T3f7Sygc8AUhS3rVnKZxWIuaZpvbg9Eq15O44R9VO8ldUetOcgQCJN5wA6bcXAK9elH6wgutftD3/lQvmfv2Wzy7Q6ai70D31zhPCbmJ33QPyeuuwhQoJODsS8Gg/+HqfAMoe/5XpBQXk3zRRViXLkV3Oin68EO8p5/eZO/HiIsjlJgIgJrnQQ3ITqpCCCHEoVIVlTO6hTvdZ2fOjvj1S7wlLNq5CJClcqJ52z38e0XOCow28sPLeoVMEydOZMOGDXz11VdUVFREuibRitR1HtOh2H930wYStjwo3U2HwgiRsPHOvZbJXfaX5w3i77kHU1ERgb59qbjzzj3PaRrlTzxB6VNPYZhMOL78kpS//Q01v25b2Nec01RO0u9XEZP1YlTnNKn+wvBOciFX1U5yj9V6Xtzjj2NdsQI9Jobid97BiI1t4kqbD9+JJ2JYLGgZGegF8YTMqaihcqyle35aYy5fQ+rK0ThzpgFQ2ek6CgZ+QzCmX41rqdnZpIwfj+XXXwklJlL06af4jzuuSd8PsGeuVr7sMCeEEELU1+65TPO2zSOoByN67blZcwkaQfom9W0zS5BEyzQgdQBWk5U8dx6Z5ZnRLqdJ1Gsm05133olhGEyfPp3p06djsVj22V0O4P33329wgaJlM3kywQDtpc2k7BiLZ/x43Oefj1HVKdAgB5rdlPspjtxPCTj74mo/AU/aeJndVAvnzncPuEzO/umn2OfNwzCbKXn5ZbDuu57efeWVBHv0IOmGG7CsXk3KmWdS/N//EjzyyIPef985TZOxFX9Pebd/4k8c2bTdQbqPxOqd5LpRfMSbte4kZ//iC2LeeQeA0pdeItRr31lNbYkRE4Nv+HBsP/6Ibf53eE8Oh0m2wrn4EkYQs/1lYrNeRCFEyNqeksNfxJ94/D7XMWVkkHzJJWg7dxJKT6fo448J9o7OF42hLl3g11/DO8x5thGIPzYqdQghhBAt2bD2w0iwJlDsLebn3J8Z0WFExK79VcZXgHQxiebPptkYkDqAFbkrWJGzgh7xPaJdUqOrV8g0dOhQWfcq6kTzZEIlWL5ZB4Bl7VrinnoKz5ln4r7sMvxDhkQkSNjd3eTueBXm8lXhb3Lzv67uborLeAJvu7NxtZ9AIG5Qm13atDeTO4PYzP8AtS+TM+3cSfy//w1AxV13EezXb59r7OYfOZKCWbNIuuoqzFu3hgeCv/wy3rFjD15I1ZymQOzRxP/5IJbylaT8dgm++CFUdLsTf8Lxjf/7ZRgkbLq3KnCLo/jI9zHM+wah2rp1xN9zDwAVt95ap4HnbYF39OiqkGk+lRfeXBUyzcFc8RuWijUAuNudS1nvJ2vdoU9bt47kCRMwFRYS7N6doo8/JtSp0z7nNZXgXjvMabLDnBBCCFEvZtXM6V1P55PNnzAna07EQqZCT2H1nKeze0rIJJq/Ye2HsSJ3BeuL1ke7lCahGG1lYWATKSgoIBAIRLuMZiNt2QBM6/LhUdDj4wl16IB5w4bq5wO9e+OeMCHc3ZQU2W0ulUBZje6m6ntKdxMYIZLXnI+1/Bd8iSMpOvqjmkGOrofn4ixbhn/wYAq//BJMpoNeVikrI/HGG7H9+CMA5ffcQ+Wtt9Y5JFJ9OcRsfx1n9jQUwwfQJGGTc/sbxGc8gYGJ4qOn1jroWykpIXXsWLTt2/GedBLF779fp89JW2DatYu0IUMwVJW81T/RbuOJqCEXALopjrI+T+FJO6/W11p+/pmkK69ELS8ncMQRFE2bhp6a2pTl78MxfToJd98N/cH97HhK+70S1XqEEEKIlmr+tvlc9e1VpDvT+eWSX1CVek1rqeH99e/zwNIHODrlaOacNycCVQrRuHJduQT0AJ1jO0e7lAYxm82k1uHr9Ib/LRdiP5RgJSZ/PuSEPw4cfTQF8+dT8M03uC65BN1ux/znn8Q/8gjpgweTcMstWFasiNgcpX1mN6X9DaPW2U2r2tzspoMtk3O++y7WZcvQ7XZKXnyxzmGKER9P8QcfUPn3vwMQN2kSCbfcAh5PnV6vW9tT3vtx8oYtpbLj3zEUK9ayn0lZezHJv47HUrI44r9X1sJvict4EoCyXo/WGjARCpF4881o27cT7NqVkldekYBpL6GOHQn064ei61h/WIo3JTyDwZcwnIJjF+w3YLJ+9x1Jl1yCWl6Ob8gQCj//POoBE/ylk8mbFdVahBBCiJZsZMeROM1Ocl25/Frwa0SuOStjFiBL5UTLke5Mb/EB06GocydTRkbGIV+8R4/Wv97wr6STaQ+tYh3tVp2O/rkddYYH1xVXUPb009XPKxUV2L/8EufUqZjX72kdDPTsGe5u+tvfGqG7qRRH3pc4cqbt293U4TI87c5r9d1NJncGqStPQ9W9lPb5D+4Ol9d4XvvzT1LPOAPF66X06adxX3FFve7j+PBD4h98ECUYxD9gAMXvvovert0hXaOxO5u0yg2krDkHNeTC1eFyyno/Xes1Y//zH2JfeQXdZqPw668JHnFEg+7bGsVOnkzsiy/iOfNMSl9/AXPlH/jjB9fYOW5v9pkzSbjtNpRgEO/JJ1Py1lsYdnsTV107044dpA0bBhqEpiaRN/L3aJckhBBCtFg3fncjX2d8zU1H38S/hv6rQdfKdeUyePpgDAx+uvgnOsVGb3m9EG1NXTuZ6hwyXXTRRYdcxCeffHLIr2npJGTaw5b/FUnrb0J/PRF1aQlljzyC69pr9z3RMDCvXYtj2jTsM2eiut3hhy2W8OymCRPwDxsW2aVShhGe3ZQ9FXvBLBTdC4Cu2qpmN11GIG5g65vdtPcyuYTjKer/cc33GAiQcs45WNauxXviiRRPndqgz4Fl6VKSrrsOtbSUUPv2FL33Xp0Ggv9VY4RNqr+QlFVnovl24ks4jqKjp9U66Ns2Zw5JEycCUPLaa3jOPfeQ79UWmNeuJXXsWHSnk9zff691SPxujvffJ/5f/0IxDNznnkvpiy+Ced/PfdQEg7Tv2RMlGISXIeecTRhaTLSrEkIIIVqkWRmzuOG7G+gW140lFy5p0Gzfd9a9w7+X/5uB7QYy65xZEaxSCHEwdQ2Z6jz4+8Ybb2xQQaLt0dxVWzTmhLekD+6vs01RCBxzDGXHHEP5v/+NfeZMHFOnYlm3DseMGThmzCDYoweuCRPwXHgheiS6m/bama6s1yPh7qbsqZjdm2ruTNfKupsOtkwu5pVXsKxdi56QQOmz+z5/qPzHHUfBN9+EB4Jv2RIeCP7SS3jPPPOQrrN7GV1ll5uqwyZr2c9Y116ML35oVdh0XN3rrd5JbmfVTnL/V2vApP35Jwm33QZA5bXXSsB0AIGjjiKUloYpLw/r8uX4Tjxx35MMg5iXXyZu0iQAXFdeSdkTT0Atu5NGlaYR6tQJLSsL8sHkySIYe+jhqBBCCCHg5M4nYzVZySrPYkPxBvol738zmYP5OuNrQJbKCdGcyeDvCJNOpj0SNtyGI+dzjIkaii9I3pIlhLp3r9uLDQPzb7+Fu5tmzKjZ3TR2bLi7afjwRuhuWokze1ot3U3nVO1M13K7m2osk+v9NO6ONZfBmdeuJeWss1BCIYpffx3vOedE7N5KWRmJN92E7YcfACi/+24qb7ut3p/L2jub6hg2GQYJm+7EkfspuimOwoGzCDp77VtzRQUpZ56JeetWfMOHU/Txx6DVa0PONiP+nntwTpuG66qrKHvyyZpPGgZxjz1GzFtvAVBx221U3H13s/37lHTJJdgWLYLroPimt/CmHlowKoQQQog9rv72ar7d9i13DryTfw76Z72usatyF0M+GoKCwi+X/kJ7Z/sIVymEOBAZ/C2iTvNkQgkoviCGphHqfAjDzhSFQP/+lE2aRN6aNZQ+8wz+o45C8ftxzJxJyt/+RruRI3G++SZqUVFkClYUAvHHUtr3RXKHr6Ks1+MEHIeh6l4cuZ+QuuZsUleehmPXeyiBssjcs6kYIRI23omqe/ElHL/PHCY8HhJuvRUlFMJz9tkRDZigaiD4++9Tec01AMRNnkzCP/5R54Hgf1X7gPCfSFl7Ecm/no+lZMl+B4Q7d7yJI/dTDEyUHPFmrQETuk7Cbbdh3rqVUPv2lLz5pgRMdeAdPRoA67ff1vz8B4Mk/POf1QFT2cMPU3HPPc02YAIIde0aPigAzbMtusUIIYQQLdyYbuFNQeZk1X83uN0Dv4ekD5GASYhmTEIm0WhMnkzIDR+HunSp9zfpRkwM7ssuo3DuXArmzME1YQK604mWmUn844+TNmgQiTfeiGXJ/oOFQ76nOQFXp79TcOx3FAyYWXNnuj//VbUz3Z0tZme6gy2Ti3vmGcxbthBq147Sv3agRIqmUf7YY5Q+8wyGpoXDwgsuQM3Lq/clDzVs2nsnufJej+BLGlXrdWNeeQX7vHkYFgvFb7+NnpJS7xrbEt9xx6HbbGjZ2Wh//BF+0Osl8YYbcHzyCYaqUvL887iuuy66hdZBcHfIVLVcTgghhBD1d1qX09AUjQ3FG8goO/QNpWCvXeV6ylI5IZozCZlEo1ACZZgCxZAT/ni/85gOUeDoo8PdTatXUzppEv7+/VECAexff03KRRfR7vjjcb7xBmphYUTud0jdTcHyyNwzwkzuDGIz/wNAeY8HCdlrdpRZli4l5u23ASh99tmI7+j3V+7LLqPoo4/QExKw/PorqWPHYv69Ybt31SVs0io3kLjhHygYuNpfhqvj1bVey7pwIbGTJwNQ9tRTBAYMaFBtbYrdjm9UOLizzZ+PUllJ8hVXYJ8zB8NioeTtt/HUYxOJaAh16RI+yJdOJiGEEKKhEm2JjOgwAoC5WXMP+fVZ5Vn8WvArqqJyZjdZwi5EcyYhk2gUmic89FvPD29JHqmQaTcjJgb3hAkUzp5Nwdy5uC6/HD0mBi0ri/gnniBt8GASb7gBy+LFoOuRuec+3U0X1OxuWjYg3N1Uvrr5dDcZOgmb/lm1TO64fZbJKRUVJNxxBwCuCRPwnXJKk5TlHzGCgm++IdC7N6bcXJLPPRfbN980+LoHCptSV49DDbnwJRxHWe8nal2qZcrKIvEf/0AxDFwTJuC+5JIG19TW7F4yZ//mG5Ivvhjr0qXoTidFH36I94wzolxd3dXoZPJKyCSEEEI01O4lc7MzZx/ya3d3MY1oP4JUx8FnwgghokdCJtEododMRn54G/NgXQd+10PgqKMo+89/wt1NkyfjP+aYcHfTrFmkXHwx7UaOJOb11xuhu+ml2rubVp/VbLqbnLvexVr2c9Uyuef2CVbiH34Ybdcugl27Uv7ww01aW6h7dwq//hrvSSeher0kXX89MS+8EJGArrawSdG9B9xJTnG7SZo4EbWsDP/AgZQ9/niD62iLfKecgqEomDduxLJmDaHERIo+/RT/8cdHu7RDUt3JVAam0p2g+6NbkBBCCNHCndHtDBQU1hSsYVflrkN67ddbq3aVk6VyQjR7EjKJRmGqCpmUnBAQ+U6m2hhOJ+5LL6Xwf/8jf948XFdcUd3dFPfkk+Hupuuvx7JoURN2Nw0kfuM/o9LdZHJnEJvxNFD7MjnbvHnhOTmKQumLL2I4nU1aH4ARF0fxe+9ROXEiAHHPPkvy+PFYf/gh4mFTWa/HKez/GYY5sZZCDOLvvhvzhg2EUlIofustsFobfP+2SE9NrV5iGEpPp+jLLwkcc0x0i6oHIy4OPSEBAKXAwOTdGd2ChBBCiBaunaMdx6YdC8C8rHl1ft2W0i2sL16PpmjV3VBCiOZLQibRKDR3JgRByXEBTRMy7S145JGUPf00eatXU/Lcc/gHDAh3N33zDSmXXEK7448n5tVXUQsKInPDfbqbHiPg6IOqe3Dmftz03U37LJO7rMbTamEh8XffDUDljTfiHzKk8WvaH02j/NFHKZ08GcNiwfrzzyRPmEDK2LHYZs+OSCCoW9vj6vR3dFuHWp93TpmCY+ZMDE2j5P/+D7297FjSEGUPP4xrwgQKZ84k2KdPtMupt72XzMlcJiGEEKLhxnSvWjKXVfclc7uXyo3sOJIkW+PODhVCNJyETKJRaJ5MKAQlpKPb7ejp6VGpw3A68Vx8MYXffBPubrrySvTYWLRt24h7+ulwd9N112GNeHfTNRQcu5DCKHU37btMbq+/6oZB/H33YSoqInD44VTcdVej1HCo3JdeSt7SpVReey263Y7lt99IuvZaUk8+Gfvnn0Mg0Cj3tSxbRlzV0rjyf/8b/7BhjXKftiQweDBlkyYR6tz54Cc3Y3sP/5a5TEIIIUTD7e5E+in3J4o8RXV6ze6lcmf1PKvR6hJCRI5iGM1lQnHrUFBQQKCRvhluMQyD9KVHoK4sg8kQ6NuXggULol1VNcXtxjZrFs6pU7GsXl39eLBLF9yXXor7oovQ27WL7D0DpTjyvsCRPRWze3ON5wz2HUDd4PsR/mtd2vtp3B2vqPGc/bPPSLz9dgyzmYJvviF45JERv39DqUVFON95B+d//4taHu78CnbuTOWNN+K+6CKw2SJzn127SB0zBlNREe7x4yl9+eVaB4KLtin26aeJffVVGA2V/7qW8l6PRLskIYQQosU7Y8YZ/F74O5NHTubSwy894LkbizdyyhenYFEt/HrZr8Rb45uoSiHEX5nNZlJTDz54XzqZRMSpgRLUYBnkhD9u6qVyB2M4HHguuojCWbPI//ZbXFddFe5u2r6duP/8h7RjjyXx2mux/vhjo3c3QTgQivQvAG/Syfsuk9u1i/iHHgKg4s47m2XABKAnJ1Nxzz3k/fQT5fffTyg5GW3HDhIeeIC04cNxvvkmisvVsJt4vSRdd124o+uIIyibNEkCJlFDjU4mWS4nhBBCRMTYbmMBmJM156Dnfp0R7mIa1WmUBExCtBDSyRRh0skE5rKVpK45B/0DB+o8NxW33ELFffdFu6wDqu5umjYNy6pV1Y8Hu3TBfckl4e6mtLTI3jTkQQ1VRvaa1RR0c3LN0ETXSb7kEqxLluAfOJDCGTNA0xrp/pGleDw4PvoI5xtvoGVnA6AnJFB5zTW4rr4aI7GWYd4HEX/33TinT0dPSKBgzpw9gYIQVSyLF5Ny8cXQAQKvHUbBkIXRLkkIIYRo8baUbmHUZ6Mwq2Z+u/w34ixxtZ5nGAYjPx1JZnkmr5z0CuN7jW/iSoUQe5NOJhE1WtXOcuSGt4lvbp1Mtanubvr6a/IXLKDy6qvR4+LC3U3PPBPubpo4MbzrWYS6mzDZ0S2pjfQrZZ+uHOd772FdsgTdbqfkpZdaTMAEYNjtuP7+d/KXLqXk+ecJdu+OWlpK3HPPkTZ0KHGPP46al1fn6zmmTsU5fTqGqlLy+usSMIlahXYP/i4AkzuryXeIFEIIIVqjXgm96J3Qm4AeYMH2/Y/U+KPoDzLLM7GZbIzuMroJKxRCNISETCLiqkOmnHBHV7B79yhWc+iCfftS/sQT4Z3pXngB/+DBKKEQ9jlzSJ4wgXYjRhDz0kuoubnRLrXOtC1biHvySQDKH3yQUAsI/mplseC56CLyf/yR4jfeINCvH6rLRcybb5I2fDjx99+PaceOA17CvGoV8Q8+CEDFvffiGzWqKSoXLVCoQwcMkwkCoBb7UP11DzKFEEIIsX9ju1ctmcvc/5K5r7Z+BcDJXU4mxhLTJHUJIRpOQiYRcZo7E/yg5rsBCPXsGeWK6sew2/FceCGFX31F/nffUXnNNejx8Wg7dhA3aRJpQ4aQeM01WBcuhFAo2uXuXzBIwm23oXi9eE84AfeVV0a7ooYzmfCefTYF335L0fvv4x80CMXnw/nBB7Q77jgSbrsN7c8/93mZWlBA0nXXoQQCeMaOpfLmm6NQvGgxNI1Qp07h43zQZC6TEEIIERG75zIt3LEQT9Czz/OGYTArYxYAZ/c4u0lrE0I0jIRMIuJMnkyoavLRExLQ6zEvp7kJHn445Y89Rt6qVZS8+CK+Y48NdzfNnUvy5ZeHu5tefLFZdjfFvPIKll9/RY+Pp/S551rXcGtFwXfqqRR+9RWFn3+Od9QolFAIx+efk3rSSSReey3m334LnxsIkHj99Zhycwn07k3pCy+0rs+FaBQ1h39nRbUWIYQQorU4IvkIOsd0xhvy8sOOH/Z5fk3BGnZU7sChOTi1y6lNXp8Qov4kZBKRZRjh5XJVq0qC3bu3qm/kDbsdz9/+RtHMmeQvXLinu2nnTuImTw53N/3971i/+65ZdDeZf/uN2BdfBKDsySfRO3SIbkGNRVHwDx9O8fTpFPzvf3jGjEExDOyzZ5M6ZgxJEyaQcMcdWH/6CT0mhuIpUzBipO1aHFxwr5BJk5BJCCGEiAhFURjTfQwAs7Nm7/P811vDu8qd1vU07Jq9SWsTQjSMhEwiotRAIWqoEiMn/HFLm8d0KIKHHUb5Y4+Ru2oVJS+/jG/IkHB307x5JF9xBe2GDyfmhRdQc3KiU6DXG14mFwziGTcOz7nnRqeOJhY45hhKpkwhf+FC3OPHY5hM2H74AceMGQCUvvwyoV69olylaCmqh3/ng8kry+WEEEKISNk9l2n+tvn4Q/7qx3VDZ1amLJUToqWSkElElOYOD/02CpxAy9hZrsHsdjznn0/RjBnkf/89lRMnoickoO3aRdyzz4a7m66+GuuCBU3a3RQ3aRLmzZsJpaZS9vTTraqjrC6Chx1G6SuvkL94Ma7LL0ePj6f8gQfwnn56tEsTLUjNTiYJmYQQQohIGdRuEO3s7agIVLA0e2n14yvzVpLryiXWHMuJnU6MXoFCiHqRkElElGn3znK5JqCNhEx7CfbpQ/mjj4a7m155Bd+wYSi6jv3bb0m+8kraDRtGzPPPo2ZnN2odluXLcb71FgClkyejJyU16v2as1DXrpT95z/krl8vg77FIavuZCqQ5XJCCCFEJKmKyhndzgBgTtaeXeZ2L5Ub3XU0Ns0WldqEEPWnRbsA0bpongwAlJxwy2uojYVM1Ww2POPH4xk/Hu3PP3FMn47j00/RsrOJe+45Yl94Ad8pp+A/5hjQdZRQKNzldIBjQiGUqv/W+txex+Y//kAxDFyXXorvtNOi/dkQosWq7mQqBdVVihIowzDHR7UmIYQQorUY030MH2z4gLlZc3n6uKcB+F/m/wA4u6cslROiJZKQSUSU5skEFyglXqB1z2Sqq2Dv3pQ//DDl996Lfc4cHNOmYV2+HNv8+djmz2+8+3bpQvnDDzfa9YVoC4yEBPT4eNSysnA3k3cbAfPR0S5LCCGEaBWGtx9OgjWBIm8RP+f9jG7o5HvySbAmcELHE6JdnhCiHiRkEhGluTMgN3wcSkuTHbz2ZrPhOe88POedh7ZlC/bPP0ctLgZVBZMJw2Tac6yq1cfVH+8+3vu83cd7v85kAk3DN2yYfP6FiIBgly5Yfv89PPzbk0UgVkImIYQQIhLMqpnRXUfz6eZPmZM5B1/IB8AZXc/AYrJEuTohRH1IyCQixzAwebKqQ6a2No/pUAR79aLivvuiXYYQog5CXbpAVcgkw7+FEEKIyBrTbQyfbv6U2Vmzq0MmWSonRMslIZOIGNWfi6p7MHIVFAxZKieEaBWCu4d/54NJQiYhhBAiok7oeAJOs5McVw4ASbYkjutwXJSrEkLUl+wuJyJGc4d3ljPyHYB0MgkhWofQ7uHf+aB5s6JaixBCCNHa2DQbp3Q+pfrjM7ufiaZKL4QQLZWETCJiNE84ZCIv/Meqze4sJ4RoVULSySSEEEI0qjHdxlQfn91DlsoJ0ZJJRCwiRvNkggHKrqqd5SRkEkK0AjWWy3mzQfeBao1uUUIIIUQrckqXU+gc05l4azxD04dGuxwhRANIyCQixuTJhHJQ3AEMVSW4e4mJEEK0YKEOHTBMJpRACKUUNM8Ogs5e0S5LCCGEaDWcZieLL1oMgEk1RbkaIURDyHI5ETGaOxPC8/oIdeoEVvlJvxCiFTCbCXXsGD7OB5PMZRJCCCEizqyaMavmaJchhGggCZlEZBg6mncb5IY/lKVyQojWpMbwb5nLJIQQQgghRK0kZBIRYfLloOhejNzwHykJmYQQrUlQhn8LIYQQQghxUBIyiYgweTIAMPLtAIS6d49mOUIIEVE1O5myolqLEEIIIYQQzZWETCIiNHc4ZJLlckKI1ii4V8hk8konkxBCCCGEELWRkElEhObJBB2UHC8gIZMQonUJ7bVcTvPsAEOPbkFCCCGEEEI0QxIyiYjQPJlQBIo/hGGx7NmJSQghWoHqTqZSULw+VF9OVOsRQgghhBCiOZKQSUSEyZ25Z6lc165gMkW3ICGEiCAjIQE9Li78QYHMZRJCCCGEEKI2EjKJhtODaN7tUPWDfVkqJ4RodRSlxlwmTXaYE0IIIYQQYh8SMokGM/l2oRgBjNxw91JIQiYhRCsUkuHfQgghhBBCHJCETKLBNE8mAEaBFZBOJiFE61Q9/FuWywkhhBBCCFErCZlEg5nc4ZCJHAOAYPfuUaxGCCEax97L5UyyXE4IIYQQQoh9SMgkGkzzZEIQlDwvIJ1MQojWqbqTafdMJsOIbkFCCCGEEEI0MxIyiQbTPJmQD4puoDud6O3aRbskIYSIuL07mdRgOUqwJLoFCSGEEEII0cxIyCQaTPNkQG74ONi9OyhKdAsSQohGEOrYEUNVwQ+UyQ5zQgghhBBC/JWETKJh9AAmzw7ICX8oO8sJIVoti4VQhw7h43wJmYQQQgghhPgrCZlEg5i821EIYeSZAJnHJIRo3UI1hn9nRbUWIYQQQgghmhsJmUSDaJ7wznJGnhWQkEkI0boF9x7+7ZVOJiGEEEIIIfYmIZNoEM0dDpmUXB2omskkhBCtVM1OJgmZhBBCCCGE2JuETKJBNE8meEEp9AISMgkhWrcanUwSMgkhhBBCCFGDhEyiQUyezOqd5UJJSRiJidEtSAghGlGNTiZ/LoQ80S1ICCGEEEKIZkRCJtEg2t4hk8xjEkK0cqHdnUwlgB807/ao1iOEEEIIIURzIiGTqD/dh8m7qzpkkqVyQojWTk9MRI+JCX9QKHOZhBBCCCGE2JuETKLeNM92FHSMPA2QneWEEG2AotRYMqd5sqJajhBCCCGEEM2JhEyi3kye8M5yRp4ZkJBJCNE2BLt1Cx/I8G8hhBBCCCFqkJBJ1JvmyQBAyQkCEjIJIdqGGsO/vRIyCSGEEEIIsZuETKLeNHcmVIBSHgAgJDOZhBBtQHB3yJQny+WEEEIIIYTYm4RMot5q7CzXvj2G3R7dgoQQoglU7zCXDybvDtCD0S1ICCGEEEKIZkJCJlFvmidjz85yslROCNFG7O5kMgpA0YOYfNlRrkgIIYQQQojmQUImUS9KyIPJlyMhkxCizQl16oShKCg+oBxMsmROCCGEEEIIQEImUU+7v6mq3llO5jEJIdoKi4VQhw7h43zQZPi3EEIIIYQQgIRMop40TyYARq4GSCeTEKJt2XuHOc0jIZMQQgghhBAgIZOoJ82TCQYoOX5AQiYhRNsS3Hv4t4RMQgghhBBCABIyiXoyuTOhFBRvCMNk2vNTfSGEaANqdjJlRbUWIYQQQgghmgst2gX81dy5c5k1axalpaV07dqVv//97/Tq1avWc3/66SdmzJhBbm4uoVCI9PR0zjrrLE444YQa58yfP5+MjAwqKyuZNGkS3bp1q3Edv9/PBx98wLJlywgEAvTv35+JEyeSkJDQiO+0ZdM8mZATPg517gxmc3QLEkKIJhTau5PJuw0MAxQlukUJIYQQQggRZc2qk2nZsmV88MEHXHDBBTzzzDN07dqVJ598krKyslrPj4mJYfz48TzxxBNMnjyZk046iddff51ff/21+hyfz8fhhx/OhAkT9nvf999/n1WrVnHnnXfy6KOPUlJSwnPPPRfpt9eqaJ5M2VlOCNFmBas6mYx8UEMu1EBRlCsSQgghhBAi+ppVyPTNN99wyimncNJJJ9GpUyeuvfZaLBYL33//fa3nH3HEEQwZMoROnTqRnp7O2LFj6dq1Kxs3bqw+54QTTuCCCy7gqKOOqvUabrebhQsXcuWVV3LkkUfSo0cPbrrpJjZt2sTmzZv3W2sgEMDtdlf/8ng8DXvzLYgSdGHy51V3MknIJIRoa6o7mUoA/54dN4UQQgghhGjLms1yuWAwSEZGBueee271Y6qqctRRRx0w7NnNMAzWrVtHdnb2AbuW/iojI4NQKFQjhOrYsSMpKSls3ryZPn361Pq6GTNm8Pnnn1d/3L17d5555pk637clM+3eWS7fjEKAYPfuUa5ICCGalp6UhO50orpcUBjeYS4QPzjaZQkhhBBCCBFVzSZkKi8vR9f1feYgJSQkkJ2dvd/Xud1urr/+eoLBIKqqcs0113D00UfX+b6lpaVomobT6azxeHx8PKWlpft93Xnnnce4ceOqP1ba0CwOrSpkItcEBKSTSQjR9igKoS5dUDds2DOXSQghhBBCiDau2YRM9WWz2Zg8eTJer5fff/+dDz74gLS0NI444ohGva/ZbMbcRodda55MCAG5PgBCPXtGtyAhhIiCYNeumKtCJtlhTgghhBBCiGYUMsXFxaGq6j7dQ6WlpQfc5U1VVdLT0wHo1q0bu3btYubMmXUOmRISEggGg7hcrhrdTGVlZbK73H5o7gwoBCVoYNhshNq3j3ZJQgjR5EJVw7/DIZN0MgkhhBBCCNFsBn9rmkaPHj1Yt25d9WO6rrNu3br9zkWqja7rBAKBOp/fo0cPTCYTv//+e/Vj2dnZFBYWHtJ925IaO8t16wZqs/ljJIQQTSa4e/h3AZgkZBJCCCGEEKL5dDIBjBs3jtdee40ePXrQq1cvZs+ejc/n48QTTwTg1VdfJSkpiUsvvRQID9/u2bMnaWlpBAIB1qxZw+LFi5k4cWL1NSsrKyksLKS4uBiger5TQkICCQkJOBwOTj75ZD744ANiYmJwOBy8++679OnTR0Km/TB5MmVnOSFEm7d3J5MpUIASdGFozgO/SAghhBBCiFasWYVMI0aMoLy8nE8//ZTS0lK6devGAw88UL1srbCwsMaAbZ/Px5QpUygqKsJisdCxY0duueUWRowYUX3OypUref3116s/fvHFFwG44IILuPDCCwG48sorURSF5557jmAwSP/+/WsEVWIPJViOKVC0p5NJQiYhRBsVrAqZjHxQjPDw72BMvyhXJYQQQgghRPQohmEY0S6iNSkoKDik5Xotjbl8Lamrx2JMMqOsDVDy/PN4Lroo2mUJIUTT83pp36sXimHAG1A8fAre1DHRrkoIIYQQQoiIM5vNpKamHvQ8GaYjDonmyQwf5IY7ykLdu0exGiGEiCKbDb1q4wnyZS6TEEIIIYQQEjKJQ2LyZIIfyPcDslxOCNG2VQ//zgfNmxXVWoQQQgghhIg2CZnEIdE8GVA1f0SPi0NPTo52SUIIETV7D//WPFlRrUUIIYQQQohok5BJHBLNnVlz6Pdeg9iFEKKtCe69w5wslxNCCCGEaLGUoAtkZHWDScgkDonmyYSc8HFQ5jEJIdq4ULdu4YN8MHl3gd56N34QQgghhGit7Lmf035JH+y5n0S7lBZPQiZRZ0qgGDVYWrOTSQgh2rDdnUxGPiiEMHl3RrkiIYQQQghxSAyDmO2vgA8cOZ9Fu5oWT0ImUWeaO7yznJFnBiAkIZMQoo0L7R78XQwEQPPKkjkhhBBCiJbEUvYT5rlb4O9gmfULhDzRLqlFk5BJ1JnmCYdM0skkhBBhenIyusOBYgCFYJLh30IIIYQQLYpj24dQ1cCkLA1hKV8Z3YJaOAmZRJ1pnkxwg1ISnjkiM5mEEG2eouzpZsoHTYZ/CyGEEEK0GEqgGPvX34S70gG2gLVgUVRraukkZBJ1ZvJkQl74OJSaihEbG92ChBCiGZAd5oQQQgghWiZH9mcos4J7HvCBbdXC6BXUCkjIJOpMc++1s5wslRNCCABCe4VMMpNJCCGEEKKFMAyc37wNu8BwWvEPHQiAtmoTSrAyysW1XBIyiboxjPByud3zmGSpnBBCABDca7mcybMNDCO6BQkhhBBCiIOylP2M9nm4i8J1+eV4Rp8JgLLJwFL2czRLa9EkZBJ1ogaKUEMVGFUhk+wsJ4QQYbs7mYx8UHUPqj8/yhUJIYQQQoiDifn2RdgMhlml8rqb8A8bFn5iI1iLl0S1tpZMQiZRJ9U7y+WZAVkuJ4QQu1UP/i5QwJDh30IIIYQQzZ0SKMb6QThI8p47Gj0tjcCRR2LYLeAG69rvolxhyyUhk6gTkzsDDCBHByRkEkKI3YKdOgGgeAyoBJM3K7oFCSGEEEKIA4pZ+jrKGh1DgfJbHgg/qGn4B1XNZVq9BSVQGr0CWzAJmUSdaJ5MqADFFcJQlD0zSIQQoq2z2Qilp4eP86WTqU5CXpzb38CR81G0KxGiRdFcW4jfdA8x217GWvwDqr/44C8SQghRk2HgfHsqAIGTjiTUs2f1U77hIwFQNoK1dEVUymvptGgXIFoGzbNnZ7lQx45gs0W3ICGEaEaCXbtiys3dM/xb7JdW+QeJG27F7NoIgD/2GIIxfaNclRAtQ2zmM9gLZ9d4LGjtRCD2aAKxR4X/G3M0uiUpShUKIUTzZ9vwP9TFFQCU3/5wjef8Q4eGDzaBpWQp3tQzmrq8Fk9CJlEnNXaWk6VyQghRQ6hLF/jpp6pOpqxol9M8GTrOHW8Rl/kMiuGvftiZ/QFlfZ6OYmFCtBB6AGvJYgC8SSejeTLDv3w70Xw7a4RPEjwJIcT+xb4xGUIQOiYN/6ARNZ7zH3MMhllDKQ1i3fgD9IlOjS2ZhEzi4AwDk3tPyCQ7ywkhRE3VS4jzweSVTqa/Mnl3kbDxNqylywHwJI/Gk3YuSetvwp73BeU9HsDQYqNcpRDNm6V8DWqogpCWSPFR74FiQgmUYa5ch7nid8yVv2GpWIvmyapD8NQff+xRGGYJnoQQbYuan4H2vy0AVNx8874n2O0E+h+FZeUazGsyUM8oRLekNHGVLZuETOKgVH8equ7GyAEFCHbvHu2ShBCiWQl16RI+yAdToBglWCGhSRV73gziNz+AGipHVx2U93oUd/tLAAg4XsDs/hN73he4O14V3UKFaOasJT8A4Es6ARQTAIY5Hn/icfgTj6s+T4InIYTYv4TXH0Lxgd7dhvuMq2s9xzfsOCwr18BGsJQuw9vu7CausmWTkEkclObJDB/kmYGALJcTQoi/CFaFTEaBioKOybONYOyRUa4qupRAKfF/PoAj/ysA/LEDKOn7CiHHnh9UuDpcScKWB3Hueg93hytBUaJVrhDNnrX4RwB8iaMOeF7kgqfwLwmehBCtheJ2Y/1kEQCea84FtfZ90PzDhsGrr8JGsJYslZDpEEnIJA5Kc2eCDuSGAJnJJIQQfxXavVyuSIdgeC5TWw6ZLCVLSNx4OyZfDgYmKrrdQWWXW0Ct+WWHJ/184jKewuz+E0vpshrfFAsh9lD9xZgr1gLgSzpwyFSbgwVPloq1mCt/k+BJCNGqxf73GZRyHaOdQvklD+73PP/gwRiqgpJvYN26CA5rwiJbAQmZxEGZPJlQAopfxzCbCXXqFO2ShBCiWdFTU9FtNlSvFwrb8PDvkJe4zGeI2fkWAEF7d0r6vkwgbmCtpxtaHJ7083Fmf4gz+30JmYTYD0vJYhQMAs6+6Nb0iFxz7+DJVfWYBE9CiFYrGMTx7nQA/JcMwrAl7vdUIzaWYL++mNetR1u7HfXkbHRbh6aqtMWTkEkclObJhJzwcbBLF9Dkj40QQtSgKIS6dkXdtKnNDv/WKteTuOFWzK4NALjaX0Z5r4cxTI59T/Z6iZkyBT0+Htd5V+LM/hBbwVxUXw66tX0TVy5E82ernsd06F1Mh6LBwZOtM4GYoyR4EkI0O/aZH6HmuiEOyq964KDn+4YOx7xufXjJXOkyPOkXNEGVrYOkBeKgNI/sLCeEEAcT6tIFc1XI1KY6mQwd5863iMt4BsXwEzKnUHrYZHwpo2s93ZSZSeINN2BZtw6A/GO/wxc/FGvZTzizp1PR/Z9NWb0QzZ9hYC0OzxDxHmQeU6Pc/oDB029YKn7bEzx5d6B5d9QaPPnjBuFuf6GETkKIpmcYxL3yAgChs9oRSB1y0Jf4hw2Dd96RkKkeJGQSB2bo4W+WdncyScgkhBC1Cu69w5ynbXQyqd5dJG68HWvpMgC8yadRetiz+93q1/bVVyTccw9qZWX1Y46pU3HdeiXWsp9w5EyjouutoJqbpH4hWgLNtRGTPxddtREM9sTx3/+it29PoG9fQp0773dwbWOqb/AUu+0FXB3/TmXnayVsEkI0GevChZi25IEVKq+6vk4bjfiHVAVRO8GyfTEc3shFtiISMokDUn05KLoXI09BwZCQSQgh9qN6+Hc+mHzZoPtAtUa3qEZkz5tJ/Ob7UUPl6Kqd8l6P4m5/ae1fuHm9xD/yCM4PPwTAN2QI7osvJvHOO3F88QUV999NyJyKyZ+HrXCO7OLSCJRAKYZqA5Mt2qWIQ7R7Vzl/wghiJ72A86OPqp/TY2IIHn44gX79CPTtS6BvX4J9+2LExDR5nQcLnhx5X2J2rSd2+8s4d70rYZMQosnEvfIMAMYpGu4+E+r0Gj0lhWCvHmhbMtB+z8Y0cjshe5fGLLPVkJBJHJDmyQwf5JqAIMHu3Q94vhBCtFW7O5mM/HAob/LuIOToFeWqIk8JlBL/579w5M8EwB87gJK+LxNy1P5DCNPWrSTdcAPm9esBqLjlFiruugtUldiXXkLbtg3bN3NxD5lA7LYXce76QEKmCDJ5thOb+R8c+V+ha/G40y7A3WECQadsldNSVM9jSjyBmIVvABDs3h3Trl2olZVYVq7EsnJljdcEu3Yl0K8fwargKdCvH6EuXZq866lG8NT5emyF3xKb9VwtYdN1GOb9D+EVQoj6Mq9ejfmXP8AEnivOxNBi6/xa39DhaFsyqpbMLcUtIVOdSMgkDkjzZEIQyAsBslxOCCH2p7qTqUABw0DzbGt1IZOlZCmJG2/D5MvBwERFt9up7HIrqLV/OWGfMYP4e+9FdbkIJSdT+vLL+E48sfp594QJxD31FM4PP6T4nLeI2fYK1rLlaJUbCcZIX3pDKIESYre9jHPXeyiGHwA1WEbMrneI2fUO/rjBuDpchid1HJjsUa5W7I8ScmMp/QmAYGFXTHl5GDYb+QsWgMmEtnUr5g0b0DZswLx+PeYNGzDl5qJt24a2bRvMmVN9Ld3pDHc9VYVOwX79CBx+OEZs3b/hatibUfGmnoE3ZbSETUKIJhP76ovhg+PA1f+6Q3qtf+hQnNOmwabw10Du9pdEvsBWSEImcUCaOxMKQNENdLsdPT0y2+YKIURrE+zcGQDFrUMlaJ5t+KJcU8ToPuIynsG5863w0ml7N0r6vkIgbmDt53s8xD/8cPgLM8A3fDglr766z/9D3BddROzkyVjWrEHdUow35XTshbNxZr9PWZ+nG/tdtU4hL87s94jd9jJqsAwAb+IJVPR4ADVQhCN7GrbCeVjKV2IpX0n8loelu6kZs5QuRzH8BK2d0H7JAsA3bBjYwsseg4cfTvDww+G886pfoxYXo1UFTub168MB1ObNqC4XllWrsKxaVeMewS5dqpfZ7V52F+rWrfG6niRsEkI0EW3LFqzffgdA4MJeBGL7H9Lr/cOGhQ8ywZqzBPoadZrn1NZJyCQOSPNk1NxZTv5SCSFE7ex2QunpmHJzq4Z/Z0W7oojQKjeQuOEWzK4NALjaT6C858MYmrP287dsIfGGGzBv2IChKFTeeisVd94J2r5fcugpKXjPOAP7rFk4p07Fde+V2AtnY8/7gvIeDxxSS3ubZ+jY878iNvMZNO8OAALOvpT3fBBf0onVp/mSTkT15eHI/QRHznQ0747q7iZf3LG4O0yQ7qZmZPc8Jl/SKKyLwjvM+UaOPOBr9KQk/Mcfj//44/c8GAyiZWTs6Xja3fWUk4O2fTva9u0wb96ea9jte2Y97bXszoiLi9ybqxE2zSM263kJm4QQEeV8/XUUAxgIriF/P+TvZUMdOxLs2AFtVzamDQVow7YSdLauLvXGICGTOCCTJ7M6ZJJ5TEIIcWDBLl2qQybN28J3mDN0nDvfJi7jPyiGn5A5mdLDnsWXMnq/L7F/8QXx992H6nYTSkmh5JVX8J9wwgFv47r8cuyzZmH/8kvK//UvAo5emN1bsOd9gbvjVRF+U62TpWQZcVsfx1L5GwAhSzrl3e8Jb7esmPY5X7emUdn1Viq7/ANrySIc2VOxFX6LtfwXrOW/SHdTM2It/gEAX8xxJCy/M3w8atShX0jTCPbpQ7BPH7znnFP9sFJcjHnjxj0dTxs2YN60CdXjwbJmDZY1a2pcJtip076znrp1A9O+f87qTFHxpo7Bm3J67WFTp2uo7HSthE1CiEOi5uTg+OILAPRzLHjSxtfrOv6hw9C+/BI2gqV0qYRMdSAhk9g/I4Tm2Q454Q9lHpMQQhxYqEsX+Pnnqk6mlhsyqd5dJG68A2vpUgC8yadSetiz6JbUWs9XPB7iHnwQ58cfA+AbMSK8PC4t7aD38o8YQbBHD7SMDOxffYX7xCuJ3/IQzl3v4+5wpXTQHoDm2kxcxpPYihYAoJtiqOxyM65O12LUpRNJUfElnbhXd9PHOLKno/l2SndTM2Dy7sTs2YqBCWOLHdXrJdSuXXh5XIQYSUn4R4zAP2LEngeDQbSsrPCSu6qOJ239erTsbLSdO9F27oRvv60+vbrrae8ld4cfjpGQcGjF7C9s2vYSzp3vSNgkhDgkMVOmoASDcBh4jj+/3t3R/qFDcVSFTNbSpbg7XhnhSlsfCZnEfpm8u1AMP0ZueKckCZmEEOLAgruHf+cTDukNHZSm3c2poWx5X5Hw5/2owTJ01U55r0dwt5+w37BH27w5vDxu06bw8rg77qDi9tvr3tmgKLgmTCD+8cdxTJ1K0cWfEpvxNGb3Ziyly/Enjjj4NdoY1ZdHbNZzOHI+QkHHUDTc7S+jotsd6JaUel0z3N10G5VdbpHupmZidxeTP24glnnhOUq+kSMbP3jVNIK9ehHs1Qvv2Xt2elRKS/fpetI2bNh/11PHjjU6ngJHHUWoLl3x+4RNz2F2bZCwSQhRZ0pZGY6pH4Y/OAvcHSbU+1rVc5m2gCV/KfRreV/bNTUJmcR+aZ7M8EGeCQjKcjkhhDiIUJfw1rZGgYJi+FB9uei2DlGuqm6UQBnxf/4LR/4MAPyxAyjp+xIhR8/9vsb+6afEP/AAqsdDKDWVkldfrTkHpo48F15I3DPPYPn9d7Q/MvGkjceZMxVn9nsSMu1FCbqI2fEmzh1voupuADwpYyjvcf8Bf58O7SZ7dzflhmc31drddBme1DOlu6kR7T2PyfZjeF6S7yDLTxuTkZCAf9iwPd9wAYRCmLKyag4ZX78+3PG0axfarl3YFiyoPt1/5JF4zj8fz3nnoafW3hlZTcImIUQ9OT/4ALXSBZ0gcHw/ArHH1PtawZ49CSUnYyoqwrS5FO3YjQRj+kWu2FZIQiaxXyZPJvhAKQwCslxOCCEOJrS7k6kgHM5r3m34W0DIZClZSsLG29F82RiYqOx6GxVdbwXVXOv5ittN/AMP4PjsMwB8xx9PySuvoLdrV6/760lJeMaNw/HllzimTsX18NU4c6ZiK5gbDuqsbXxnUz2II/djYrOew+TPB8LdLeU9HsKfMKTxbmtNr+pu+gfW4kU4cqb9pbvp31XdTZcRdPZptDraJD2ItWQJAH4GELvuOeDgQ7+bnMlEqGdPQj174h03rvphpbwc88aNe5bcrV+Ped06LFW/4p54At+oUbgvuADv6NFgP0BYKWGTEOJQeL0433knfDwOXB0ua1gHqKLgHzoU++zZsAmsJUslZDoI6fMS+6W5MyAvfKwnJGAkJUW3ICGEaOaCVZ1MFAYh2ALmMuk+4rY+TvLai9B82QRt3SgcMIOK7v/cb8CkbdpEytixOD77DENVKb/rLoqmT693wLSb+7LLALDPmEFI74QvfggKIZzZ0xp03RbNMLAWzid15akkbL4Xkz+foK0bxf3epHDA140aMNWgmPAln0TJkVPIG/4z5d3vIWjthBosI2bXO7T75SSS15yHPfdzCHmapqZWzlKxBjVUjq4loP5agmIYBPr2rdOcs+bAiIvDP2QI7quuomzSJAq/+Ybc1aspfeop/AMHooRC2BYuJOmmm0gfMID4f/4Ty/LloOv7v2hV2FQw+FuKj5hCwNkXNVRJ7LaXSFsxjNjMSSiBkqZ7k0KIZsnx2WeYCgogGfTjbHjSzmvwNf1Dh4YPNoKldFmDr9faScgk9kvbe2c56WISQoiD0tu1w7DZUHSgCDRPVrRL2i+tcgOpq84kZsebKBi42k+gYPC3BOIH1f4Cw8D+8cekjB2L+c8/CaWlUfTpp1TecUfDdpaq4h8yhEDv3qgeD/Yvv8TV4SoAHDnTQA80+Potjbl8Lclr/0byuqswu/8kpCVS1usx8od8j7fdWVEbiL67uyl/2DKKjpqKJ2UMBiasZT+TuPE20pcPJu7Pf6O5Nkelvtaiele5xBOwLg53NEVzqVwkGElJuK+8ksJZs8hbtIiK228n2LkzakUFzo8/JuWCC2g3fDixzzyDacuW/V+oRtj0toRNQog9QiFi3nwzfDwWPB3Ow9DiGnzZ6pBpM1iLl4EebPA1WzMJmcR+aZ7MPTvLyTwmIYQ4OEXZ082UD1pz7GQydJw73iJ11ZmYXRsImZMpOvK/lB02CUNz1voSxeUi4bbbSPznP1G9XryjRlHw7bf4hw+PXF2KUt3N5Jw6FW/KGYTMqZj8edgK50buPs2cybODhPU3k7p6LNbS5RiKlYrON5M/dCmuTteAaqn/xQ0jcoXut7upVLqbImD3PCZv4ihsP1bNZmrhIdPeQj17UnH33eQvW0bhl1/iuvRS9NhYtJ07iX35ZdJGjSJl3Dgc772HWlxc+0UUFW/q2P2ETcOJzZwsYZMQbYztf/9Dy8rCiAFObNjA770F+vVDj4kBD6iZlZgr10Xkuq2VhEyidnoQk3eHdDIJIcQhCu0VMpm8zStkUr3ZJK+9hPitj6IYPrxJp1Bw7Hf4Ukbv9zXahg3h5XFffBFeHnfvvRRPnYqeUr9dzA7EfcEFGDZbeIjwr+twd7gUAOeu9yN+r+ZGCZQSt+Ux2v18Ao78mRgouNPOJ3/oYip6PoBhjq/fdcvKiHviCdL79KHd8ccT8+qrqPn5Ea193+6mM6S7qQGUQDHmil8BCJZ2xZSTg2G17vlJemuiqviHDqVs8mRy16yh+I038J5yCobJhGXNGhL+9S/SBg4k8e9/xzZ7Nvh8+16j1rCpgthtL0rYJERbYhjEvP46AMpoCCQ3bOB3DSYT/iFVS9Q3glWWzB2QhEyiVibvDhQjiJEbbseXkEkIIeomuHv4dzPrZLLlf0W7ladiLV2Crtop7fMfio96H92ynx2eDAPHtGmkjhuHecsWQunpFH32GZW33gpq43z5YCQk4DnrLACcH36Iq/1lVWHFcjTXpka5Z9TpPpw7/o+0n44jZuf/oRh+fAnHUzBoLqV9XyZk61i/6/r9OKdMIW3ECGLeeAPV5ULLyiLu6adJO/ZYEidOxPrddxAKRe69VHc3vRPubup2N0FrR+luOkTWksUoGASch2NZvh4A/7HHYhxoOHZrYLfjPftsij/4gLxVqyh79FH8Rx2FEghgnzePpGuvJX3gQOLvuw/zypX7dubVKWwqjcpbE0I0PsvixVh+/x3DosDoCAz8/ou9QyZLydKIXbc1kpBJ1ErzZIYP8sJ/RCRkEkKIutm7k0kNlkb9mxolUEbC+ltIWn8TarAMf+wxFAyeh7vD5fv94kuprCThlltIuOceFK8X70knhZfH7b11eSNx7R4APmsWhteBN+V0oBV2Mxk69ryZtPt5FPFbH0MNlhJwHk7RUR9S1P9jgrFH1vO6Brb//Y92J51E/MMPo5aWEujTh+J336Xk+efxDxqEEgxinzOH5CuuIG3oUGKffRbTzp0RfXu6NZ3KbreTP2w5RUd9KN1Nh8BWPY9pFNZFi8LHo0ZFsaKmp6em4po4kcK5c8lfuJCKm28mlJ6OWlqK88MPST3nnHBn3gsvYNr2lzD/gGHTMAmbhGilYnd3MZ1ooMfbIzLwe2/VXwNtAkvpT6D7I3r91kQxjEgu0BcFBQUEAi1/QKlz5xTi1z4M14U/ztm8GcNZ+6wOIYQQe1i//Zbkq6/G6K6hPBGkYOBsAnH9o1KLpWQZCRtvQ/NlY6BS2fU2Krrett+d4wC0P/4g6YYb0DIyMEwmKu69l8obb2y07qV9GAapp52GecMGyh57jMD4PqSsvRjd5CRv+GoMLaZp6mhEltLlxG19AkvVkqiQJZ3y7nfjSf8bKPUfom5euZL4xx/HsnJl+LqpqVTcfTfuiy4CTas+T9u0Ccf06Tg+/xy1tBQAQ1HwnXgi7ksuwXvaaWBpwOyn/VB9uThyPsaRMx3Nt6v6cV/8ENztJ+BJPRNMrbxb52AMg7TlgzH5cynq+yGJx1+P6naTP28ewSPrGTy2FqEQlmXLcHz+ObbZs1Hd7uqnfEOG4LngAjzjxmHE/2VpqaFjK5xLbNbzmF0bANBNsbg6XUNlp2sxzAlN+CaEEI3B/NtvpI4Zg6EqKM8buI66mLLDn4vsTXw+2vfti+LzwWQoPHMm/vhjI3uPZs5sNpOaup8O+L1IJ5Oolebes7NcKD1dAiYhhKijUPVyufDPcEzR2GFO9xG39QmS116I5ssmaOtG4YAZVHS/a/8Bk2Hg+PBDUs86Cy0jg1D79hR98QWVN9/cdAETgKJUdzM5pk7FH38cAXtP1JALe97nTVdHI9Bcf5L0+1Wk/HoBlopf0U1OyrvdTf7QxXjaX1zvgMmUlUXi9deTes45WFauRLfbqbjjDvKXLsU9YUKNgAkgeNhhlD/6KLmrVlH8+uv4jjsOxTCwff89SdddR9qxxxL3xBOYtm6NxNuuVqfupi0Pt+nuJs21EZM/F121YWzVUN1uQsnJBPv1i3Zp0Wcy4R85ktKXXiJv7VpKXn4Z7wknYCgK1p9/JuGee0gfMIDE66/HOn8+7P6hr3Q2CdHqxbz2WvhguAKp4O5wWeRvYrXiHzgwfLwRLCVLIn+PVkI6mSKstXQyJa29FNusH+EN8A0fTtHnLfsLeyGEaCqKx0P7Xr3CH7wF5UfeQ2XX25rs/lrlRhI33ILZFZ7l4mp/KeU9H9nvznEASkUFCffcg/3rrwHwnnIKJS++iJGU1CQ171NPeTlpAweiejwUfvkl5o7riN/ybwKOPhQcuzCiMxaagurLJzbrORw5H6EQwsCEu8MEKrrduf+ZWHWgFBcT+9JLON9/HyUQwFAU3BdfTMVdd6Gnpx/StUxZWTg++gjHp59i2mswuG/4cNyXXIJn7FhohJlAqi8HR84n++luugxP6tg21d3k3P4m8RmP4006mcCcI4l9+WXc555L6e5voMQ+1Jwc7DNn4vj8c8wbN1Y/HkpOxnPuuXjOP5/A0Ufv+XfD0LEVziE264W/dDZNpLLTROlsEqKFMWVm0u6EE1B0HZ6GQN9+FAz+tlG+VoidNInYl16C48F3/wiKjvks4vdozqSTSTSI5smUneWEEKIeDLudULt24Q+acvi3oePc8Tapq8Zidq0nZE6i+Mh3KTts8gEDJm3dOlLPOAP7119jaBplDz1E8XvvRS1gAjDi4vCcey4Q7mZyp/8NXXVgdm/GUrYianUdKiXkJibrBdr9dBzOnKkohPAkn07BsQsp6/N0/QMmrxfnm2+SdtxxxEyZghIIhOdmzZ9P2bPPHnLABBDq1o2K++8n7+efKX733fAOX6qKdflyEm+9lfRBg4h78EG09evrV/N+6Nb2B+huunWv7qY/I3rf5spW8gMAvqRRWBcvDh+fcEIUK2r+9Pbtcd14IwULFpA/bx6V115LKCUFU1ERMe+8Q+rYsaSedFJ4Z8Vdu6o6m86s6mx6a6/OpheqOpuelc4mIVqQmDffRNF19EFO6AKuDhMa7YdR1XOZNoKlbBWEvI1yn5ZOOpkirFV0Mul+2i/qifKqDsuh7KGHcN1wQ7SrEkKIFiOlatkSt4Jv9DCKBnzRqPdTvdkkbrwDa2m4ddubdDKlhz2Hbm23/xcZBo733yf+0UdR/H6CHTpQ8sYbBAYPbtRa68r866+knnkmhtVK7sqVxBU8gzNnKp7UcZQc8X/RLu/A9CCO3E+IzXoWkz/cFeSPHUB5z4fwJzRgG3pdx/7118T+5z9oO3YAEOjbl/J//7tRggg1OxvHJ5/g+PhjtL0Gg/sHDAh3N51zDkZM5Gdk7b+7aehes5tsEb9vtCkhD+lLjkAxfBT0nkXK0LNRDIPclSvR27ePdnktSzCIddEi7J9/jn3ePBRv+BtBQ1HwjxiB+4IL8I4dG/7zK51NQrRYan4+acOGheckPQj6EXbyRqzG0OIa5X6Ky0V6374ooRC8BIWnfIo/8bhGuVdzJJ1Mot40z3YUdIxc2VlOCCHqI7jXDnNaI89ksuV/TbuVp2ItXYKu2ijt/TTFR31wwIBJKS8n8frrSfjXv1D8frynnUbBt982m4AJINC/P/4jj0Tx+XB89hmujlcCYCuci+rLjXJ1+2EYWIsWkLpyNAmb78Hkzydo60pxvzcoHDirQQGTZcUKUs46i8Sbb0bbsYNQejolzz9Pwbx5jdbponfoQOUdd5C/fDlF06fjOfNMDLMZy5o1JNxzD2kDBhB/112YV6/edzv5htz3r91NyadXdTf9VNXdNKhVdjdZSpejGD6C1o6YVu9CMQwCffpIwFQfmobv5JMpff11ctesoeS55/ANH45iGFiXLiXxjjtI69+fhFtuwfrjIrxJZ0hnkxAtkPOdd1B8PkJ9k+Bw8LQ7p9ECJgDD6SRw1FHhDzaBtXRpo92rJZOQSezD5MkAA8gJfxySkEkIIQ7JnuHfYPLnQsgT8XsowXISNtxC0vobUYNl+GP7UzB4Hu6OVxywTdz822/h5XH/+194edzDD1P83/9iJCZGvMYGURTcl18OgHPqVILOvvjih6AYQRw506Nc3L7MFb+RvPZCkn+/ErN7E7qWQFnPR8gf8j3edmfXu3XftGULiX//Oynnn4/l11/RnU7K77mH/CVL8Fx0EZjqvxtdnakqvlGjKHnrLfJWrqTsoYcI9OyJ6nbj/OgjUs86i9TTTgt/sV9SErn7KiZ8ySdTctS75A3/ifJudxG0dkQNlhKzcwrtfjmR5DXjsed+0SqWLFiLfwDAl3TinqVyI0dGsaLWwYiLw3PxxRR9/jl5K1ZQfs89BHv0QPV6cXz5JckTJlQNu3+SYEF3CZuEaCGUigqcH3wAgHpGOSjg7jCh0e/rH1r1A6ONYC2RkKk2EjKJfWieTCgDxatjqOqen8gLIYSok93/bhoF4QBA8+6I6PUtpctJ/eVUHHlfYqBS0fV2Cgd8RcjRa/8vMgyc775LyjnnoG3bRrBTJwpnzMB13XXNdpC259xz0Z1OtIwMLMuW4e4Q7mZyZk8FvXksTTd5dpCw/hZSV43BWroMQ7FS2flG8oYuw9X5WlCt9bquWlhI/AMP0O7kk7HPm4dhMuG6/HLyly6l8rbbMBphCHdd6CkpuG64gYIff6Twyy9xn38+hs2GecMG4v/9b9IHDSLhlluwLFvWCN1Nd7Tq7iZryY8A+BJPwPpj1fGoUdEsqdUJde5M5W23kb9oEQXffIPrqqsIJSZiyssj5s03aXfaaaSOPh3Tlzsp6jJVwiYhmjHH1Kmo5eWEuqWgDAwScPYjEDug0e+7d8hkrvgVJehq9Hu2NDKTKcJaw0ym+M334fzuQ3gCgl27kr9sWbRLEkKIFsXy00+kjB+PkW5GeS5A0ZH/xZcyuuEX1n3EZj5LzI43UDAI2rpS0vclAvHHHvBlSlkZCXfdhX32bAA8Z5xB6XPPYSQkNLymRhZ/7704p07Fc/bZlLz2EmnLh2AKFFDc7//wthsXtbqUQCmx21/BufNdFMMPgDttPBXd7yVk61T/C3s8xEyZEh5SXFkJgPe00yj/178I9u4didIjTikrwz5jBs5p0zDvNRg82K0b7ksvxX3hheh1mOFwqMKzmz7GkfPRvrObOlyGJ2Vsi5ndZPLuIm3FEAxMFHSYRbuTxmKYzeSuX4/hcES7vNbN78f2/ffYP/8c24IFKP7w32ejqoPPM/48jMEKsXmvYXaFd6/TTXF7zWyKj2b1QrRNPh9pw4djyssjeHMa2og8Sns/ibvjVY1+a6W4mPa7l8y9AUXHTcWXfFKj37c5kJlMot40t+wsJ4QQDbFnJlMQgpHZYU5zbSJ11Thid7yOgoEr/RIKBn970IDJ/Ouv4eVxs2djmM2UPfYYJVOmtIiACcBVtWTONmcOanE57g6XAuDMfi86Bek+nDveIu2n44jZ8SaK4ceXcBwFg+ZS2veV+gdMuo79s89IGzmSuP/8B7WyEv/RR1P42WcUv/desw2YAIz4eNxXXUXBt99SMHs2rgkTwh1oWVnEPfUUaYMHkzhxItaFCyEUith9a3Y3fVCzu2nDLS2qu2n3UrlA3AAsy9YA4B88WAKmpmCx4D39dErefpvc1aspffpp/IMGoeg6tu+/J/GWW0k49T4C04+i3HUnAfthqKFyYrc9X9XZ9BxKoCza70KINsXx5ZeY8vIItUtCOzYPXbXjSRvfJPc2kpIIHHZY+AOZy1QrLdoFiObH5MmsnsckIZMQQhw6PS0Nw2oN73ZSDCZvA0ImQ8e5613itj6FYvgImZMo6zMZb+oZB3mdgXPKFOKefBIlECDYpUt497hjjql/LVEQPPJI/AMGYFmzBscnn+C6ZgIx217BWroczbWZoLNPk9WiVf5B0h/XVQ9zDzgOo7znv/AlndygJYeWxYuJf/xxzH/8AUCwY0cq7r8fzznngNqCfh6oKAT696esf3/KH34Y26xZOKdNw7J6NfY5c7DPmUOwQwc8F1+M++KLCXXsGKH7mvAln4Iv+ZR9uptidk4hZueUZt/dtDtk8iadiHXRIoBGG+gu9s9ITMR9xRW4r7gCU0YGji+/xP7FF2jbt+P49DP4lPCf4TPOQBuwCXNSJrHbnse5c4p0NgnRVHQd5xtvABA6ryMmc3GjD/z+K//QoZg3bYKNYCmVVT9/1YK+chFNIuTB5MuGvPCHwe7do1uPEEK0RKpKsHPn8HF+/TuZVF8Oyb9dSvyWh1EMH96kkykY/N1BAyaltJTEa64h/pFHUAIBPGPHUjB3bosLmHZzXXYZAI5p09At7fGmnA6Ac9f7TVaDLe8rUlafjebJImRJo7TPZAoGf4sv+ZR6B0zapk0kXX45KRdfjPmPP9Dj4ij/17/IX7QIz3nntayA6S8MpxPPxRdTOGsW+d99R+U116AnJKBlZxP7/PO0GzqUpMsuwzZ7NkRwzEDt3U3qX7qbHmle3U16EGvJEgB8ccdjrRpTIPOYoivUowcVd91F/rJl4fl1Eyagx8WhZWdjf3cu5lsyCT7ZldD37VBLpLNJiKZimzcP89at6HFxmIdsAppm4PfefMOGhQ82gbnid/k7/xcykynCWvpMJq1yI+1WnoJxr4qyU6do+nT5IkcIIeoh6fLLsS1cCNdA8Mwe5A9dfEivt+V/TcLm+1GDpeiqjfKeD4UHXx8k0DCvXk3ijTei7dyJYbFQ9u9/477qqmY73LsuFLebtIEDUSsqKProI4wjIeW3S9BNMeQNX4WhxTTezfUgcZlPE7PjTQC8iSdS0u9VDHP9d+NT8/KIfe45HB99hKLrGJqG68orqbz9dvSkpAgV3gx5vdjnzsUxbVp1kAIQSknBfeGFuC+5pFF2tN3T3TQdzZdd/bgvfihlfSYRdB5gYH4TsJT9Qsqac9G1BIqs75J67nj0hARyf/utaXYPFHXn9WKbPx/H559j/eEHlGAQAENTMQY4UIdXwgDQ7TKzSYhGYRiknHUWljVr8F51ArbTFhFw9qVg8Pwm/TpHzckhffBgDBWU/4OiIRGavdnMyUwmUS+aJxN0IDecPcpyOSGEqJ9g167hg3wweXeAUbdZNEqwnIQNt5K0/kbUYCn+mKMpGDwvPMzyQF9AGQbO//s/Us47D23nToLdulH41Ve4r766RQdMAIbDgef88wFwfPgh/sSRBOw9UUOV2PO+aLT7KoFikn+/rDpgqujyD4qP/qDeAZPidhPz/PO0O/54nNOmoeg6nrFjyf/+e8ofe6x1B0wANhuec8+l6LPPyFuyhIp//INQaiqmwkJiX3+dtJEjSb7gAuxffgkeT8Ruu6e7aUVVd9Po6u6mhI23RnQXvPrYvVTOlzgS26KqjqaRIyVgao5sNrxnnUXx+++Tt2oVZY89hr9/f5SgjvpLJbwMxj9U1LfKif32edKWD5XOJiEiyLJ0KZY1azCsVkyjdgLg6nBZk3+do7dvT7BrVxQd+JPqblQRJiGTqEHzZEIhKEEDw2ol1KFDtEsSQogWKVQVMhn5CooRwOTLOehrLKUrSP3lNBx5X2CgUtH1NgoHfk3IceBOC6WkhKSrryb+scdQgkE848ZRMGcOgaOPjsh7aQ52L5mzzZuHmp+Pu+OVQNWSuUYICbTKP0hdNRZryWJ01U5xvzep6HE/KPX4xj8UwjF9Ou2OP564555DdbvxDxxI4cyZlLz9dqN07zR3oe7dqbj/fvJ++YXid97Be/LJGKqKdflyEm+5hfTBg4l76CG0DRsid9Oq2U0lR/2X/KFL0E1OLBVrsRXOidw96sFa/CMg85haGj0lBdc111A4ezb5P/wQDk3bt0dx6bAQeAzU2yqIff550mYOISbreQmbhGgIn4/4Bx8EwDv+ZMyWDHTV1mQDv//KP2RI+GATWGUuUw0SMokaTJ69dpbr2lV+iiaEEPVU3clUaAbAVDUsula6n9itT5H86wVovp0EbV0oHPAlFd3vAdV8wPuYV64kdfRobPPnY1itlD71FCVvvokR13QDMJtCsG9f/IMHo4RCOD7+GHfaBeiqHbN7E5ayFRG9V/X8Je8OgrauFA6chbfdWYd+IcPA+v33pI4eTcLdd4e3Wu7aleI336Tw66/xH3vgnQHbBLMZ7xlnUPzhh+StWEH5XXcR7NgRtbSUmHffpd2pp5IybhyO6dNRXK6I3TZk74qr07UAxGZOqnOnYaQpgWLMFb8C4NcGYV4T3llOQqaWJdi7dzg0/flnCj/5BPeFF6I7nZAPfAnq7ZXEXfUcaU8MJOa3pyRsEqIeYl57DfOffxJKScH4W/hrI28TD/zeW/Vcpo1gdm1A9RdFpY7mSEImUYPm3itkaoM/WRVCiEgJdekSPsjTgf0P/9Zcm0hdPY7YHa+hYOBKv5iCwfMJxB8kgKjaXSXl/PPRsrMJdutGwddf477y4HObWqrqAeDTp2OoMdU/vYzYAHA9SNzWJ0jacBOq7sWbeCIFg/5HMKbvIV9KW7eO5EsuIfmyyzBv3IiekEDZI4+Q//33eM86q9X+HjWE3rEjlXfcQf7y5RRNm4Zn7FgMTcOyZg0Jd99N2oABxN99dziIiUD3WmXn69G1BMzuPxt12eWBWEsWo2AQcB6OedVWlFCIYI8ehDp1iko9ooFUFf/xx1P6wgvk/forJa++infUKAxVgT9BneIl7qzXSL/waBI+vAnFVRDtig/OMFCzs7EuWEDMyy+TeOONxLzwQtSXmYq2Rdu0idiXXwag/JH7sfu+BaqWykWJf+hQAIytCvhll7m9adEuQDQvmicTqlZ0SMgkhBD1tztkUiqD4ALTX0MmQ8e567/EbX0SxfAR0hIpO2wy3tQxB722WlxMwm23hQeLA+5zzqHsmWcwYmMj/j6aE8+4ccQ/8gjazp1Yf/wR15ArceZMw1Y4B9WXh25Nq/e1lUAxSetvwloSHtBe0eUf4U6yQ1wep2ZnEzd5MvbPPkMxDAyLBdfVV1Nx660YCQn1rq9NMZnwnXgivhNPRC0sxP7ZZzinT0fLyMA5fTrO6dMJ9O2L+9JLcY8fX+/Pq6HFUdHlH8RnPEFs5nN42p0DqjWy7+UgbFVL5XyJo7B+WbVsTjZcaRUMhwPPeefhOe881Lw87DO+xPnxf9H+3IXycxDHz19h//dXhLp3JdDnaIK9ehHs2TP8q0cPjJhG3NBgf/x+tM2bMa9fH/71xx+Y169HLS2tcZod8J55JsE+fZq+RtH2hEIk3HVXeLfc0aNRB1eibPUScPYlEDsgemV160aoXTtM+fmwFazdltWv67kVkpBJVFNCbkz+3OpOprY4I0IIISLFcDjCQ40LCiAftG5Z1c+pvhwSNt6JrSQ8f8WbdBKlhz1Xp5DE8vPPJN54I6bcXAyrlbLHHsM9YULb6Iyx23FfcAExU6bg+PBDSk7+L764Y7GW/4IjZzqV3e6o12W1yj9IWjcRzbsdXbVTevjzeNudfUjXUCoqiHn9dZxvvYXq9QLh8K/ivvv2dLWJQ6anpOC68UZcN9yA5aefcEybhn32bMwbNhD/0EPEPfEEnjPPxH3ppfiHDTvkvweujlcRs3MKmm8nzuxpuDr9vZHeSS0Mo3oeky/pROIX3R8+lqVyrY6elobrhhtx3XAj2h/riP1wMrZvvkcpCaFt2oa2ad9O11B6OsEePfYETz17EuzVi1DHjhEZZ6EWF6P98Ud1kGRevx5tyxaUWnbJNkwmgr16ETjiiPC5GzdimzuXSgmZRBNwfPABltWr0WNiKHviCZJ3XQ5EZ+B3DYqCf+hQ7LNmwSawDFwavVqaGcUwpNcxkgoKCgjU8o9zS6BV/kG7laMx7lRR8nQKv/gi/AWbEEKIekk5+2wsq1bBreA/5UgKB8/Dlv8NCZvvRQ2WYqg2yno+hLtDHZa46Toxr79O7KRJ1Utqit98k+ARRzTNm2kmtD//pN2JJ2KoKnk//YTV9DOJG24mZEknb9iKg86w+itb3lckbLoTVfcStHWl+MgpBGP61f0CwSCOadOIfe45TEXheQy+oUMpf+ghAgOi9xPW1kwpLcU+YwbOadMw7zUYPNi9e7i76W9/Q6/DFsu7OXZ9QMKf9xMyp5A/bDmGydEYZe9Dq9xIu5WnoKs2CjrPJe34EzE0jdx161p9V6IA/C4SFt2B47f/QQ4ES7oSKkpFy8jCVFi435cZVivB7t33DaB69sSIj9/3BaEQWmZmOFDa3aG0fj2m3Nxar6/HxxPo1y/864gjCPbrR6B3b7DZAHBMm0bCPffg79+fwtmzI/KpEGJ/TLt2kXrSSaguF6VPPUXw3H6krDkXXbWRN2JN1OYx7eb4739JePBBjKNAuQ9yh69Ct6ZHtabGZDabSa3D/1+lk0lU09yZEAAKwvNDZLmcEEI0TLBr13DIlA+aJ4uEDbfhyPscAH/MUZT2fZWg88A7xwGoRUXh5XHffw+Ae/x4yp5+OjrLKaIs2Ls3vmHDsK5YgePjj6m8/WbitqRg8udiK5yHt924ul1IDxKX+R9idrwBgDdxFCX9XsMwJ9bt9YaBdf584p58EvOWLeHaevSg/MEH8Y4e3TY6y6LESEjAffXVuK+6CvPatTimT8c+cyZaZiZxTz5J7DPP4B09Gvell4a7gg7S9eFufwkxO/4PzZuFc+cUKrve2iTvw1ryAwD+hOFYl/4UPh44UAKmtsLipPSU/8Pf933itzyMZmzDcDooPHIWuj8ebevWfX9lZqL4fJg3bsS8ceM+lwylpFQHTihKuDtpw4bq7sq/Cnbrtk+gFOrY8YD/fnlHj8a4914sa9ei7tqF3rFjxD4lQtRgGMTfdx+qy4VvyBDcl11G8u+XAuBpd27UAybYM5eJP1UI6VhLl0Vtt7vmREImUU3zZEI+KDroMTGH9FNAIYQQ+6peJpUPaqgSR97nGKhUdrmZim53gmo56DUsK1aQePPN4eVxNhtlTzyB++KL23SI4b7sMqwrVuCcPp3KW2/F3f5SYre/jDP7/TqFTEqghMT1N1UvV6zofDMVPe6t8/wl89q1xD3+ONblywEIJSVR8c9/hpctmg+tk0o0gKIQOOYYyo45hvKHH8b+9dc4pk3DsmYN9tmzsc+eTbBjR9wXX4z7oov2/82waqai+10kbvgHMdvfwNXh8rqHjQ1g3Xse049Vx7JUrm1RFNwdryLo7EviH9dhdm0gddUYSvq9gW/gCQQGDqx5fiiEaefOfcOnjAxMubmYCgsxFRZi/emnGi/TbTaCffvWDJT69j30H1QYBmbTBoIDDse8egO2b7/FffXVDfwkNAG/n4Tbb0ctL8d3wgn4TjyRYO/ebfr/oy2B/auvsC1ciGGxUDZ5MtbSH7CWLsFQLFR2vT3a5QEQPPxw9Ph41LIy2AaWjkslZEJCJrEXzfOXneXkH14hhGiQYFXIpBdaUfERtHWhtO/L+A+2cxyEl8e98gqxzz6LousEevWi5M03CfY99J3OWhvP2LHEJSVhysnBunAhrhMuI2b7q1hLl6G5NhN07n9OiFa5nqR119Rr/pJaXEzcww/j+PJLAAybjcqJE6m8+WaMuOj/RLUtM5xO3JdcgvuSS9A2bMDx0Uc4vvgCbdcu4p57jtjnn8d30km4L70U76mn7hMGetqdQ8z21zC7NhCz400qetzfqPUqIQ/W0nAQ4IsfSezSF8LHEjK1Sf6EoRQMmkPSH9diqfiVpN8mUN7jAVydb6j59bjJRKhrV0Jdu+I7+eQa11AqKtAyMqqDJ0Kh6lAp1L17g+c4aa4/if/zX1hLl2L0NcFqsM+Z0yJCJtt33+H46qvw8fffw6OPEuzQIbzBwKhR+EaOrH2poYgatbiYuIceAqDittsI9uhG4sprAXB1uoaQvXM0y9tDVfEfeyy2BQtgI1j7yQ5zAGq0CxDNh8mdsSdk6t49usUIIUQrEOraFQCjJIHybndRMPjbOgVMamEhSRMmEDdpEoqu4z7/fApnz5aAaTerFc+FFwLg/PBDdFtHvCmjAXBkf7Dfl9nyvyJl9dlo3u0EbV0oHPh1nQMm68KFpJ5yCo4vv8RQFNwXXEDeokVU3H+/BEzNTLBvX8ofe4zcVasoefVVfMOHoxgGtoULSZo4kbRjjyX2qacwZWbueZGiUt79HgCcO6eg+vIatUZL6QoUw0fQ2gHlTxdqWVl4Fk7//o16X9F86bYOFB7zBe70i1DQic94goQNN6OE3HV6vREbS6B/fzzjx1Nx991U3Hcf3rPPJtSrV4MCJiXoIm7rE6SuPBVraXiwsTIoBIQ7bZWSknpfu6nYZs0CwDd8GN5RozCsVrTsbJzTp5N0/fWkH3UUKeecQ8wLL2BeswZCoShXLOIeeQRTcTGBvn2pvOkmHLkfY3ZvRtcSqOhyS7TLq8FXNcPY2KSgebdj8myPckXRJyGTqKZ5MiEnfCw7ywkhRMPt7mRSc4qo7HQLhnbwWSuWZctIHT0a26JF6DYbJc8/T+lLL2E4nY1dbovimjABAOv332PauRNXhysBcOR+hhKsrHmyESJu6xMkrb8JVffgTRxFwaDZdRrwrbjdxN93H8mXX44pP59Anz4UfvMNpS+9JLNImjubDc9551H0+efkLV5Mxc03V+/4GPvaa6QdfzyOqVOrT/cln4Y/bhCq7iV220uNWpq1ODxfzZd0ItbFi8PHxx0HmiwyaNNMNkoPe47S3k9iKBqO/K9IWX1OdL5pNQxs+V/T7ucTiNnxBooRxJt8Gq4OV0Ea6N2cKKFQuIOjGVM8HmzzvwXAMnYVxv1WSr97kuL336Zy4kQCvXujhEJYVq4k7tlnSR03jvSjjybxxhuxf/IJ6n4GpIvGY/3hBxxffIGhKJROnoyi+onNfBaAim53YpibV9eZf8iQ8MEmE+hgKZVuJgmZRJjuQzcnYuSGW3Jl6LcQQjScnp6OYbGgBIOYcnIOfHIoRMwLL5B80UWY8vII9O5N4ezZeC66SJYv1yLUowe+445DMQwc06fjTzyeoL0HaqgSe94X1ecpgRKSfrusesB3ReebKD76wzrN3DGvWkXqaafh/PBDAConTqRg9mwCxxzTKO9JNJ5Qjx5UPPAAeb/8QvGUKXirlqXFPfUUSllZ+CRFobz7fQA4cqZh8uy7rXykWEv2mse0KDwbTJbKCaB6TlNR/08JmVMwu9aTumoMluJFTVaC5vqT5LUXkbT+Rkz+XIK2LhQd+R7FR71HZadrwmUODHdY2ebNa7K66sP63Xeobg+kgtI9gL3oWxK33UWi+R+YLthO5ce3kLdsIaWTJuEZOxY9Lg61tBT711+TeOedpA8aROqppxL3xBNYFi8Gny/ab6lVU1wu4u+9FwDXNdcQGDCAmB1vYAoUELR3x9Xh8ihXuK/AUUeh2+0oFUHIprrjry2TkEmEqVYKhvyAXtwOkJBJCCEiQlUJdg7PDTBt2/83rGp+PsmXXkpc1fwl94UXhpfHHXZYU1XaIrkuD3+x6fjoIwiGcHUMdzM5sz8Aw0CrXE/qqrHYShahq3aK+71ORc9/HXzAdyBA7KRJpJx7LlpWFsEOHSj85BPKH30U7PbGfluiMZnNeMeMoXjqVAKHHYZaVkbMm29WP+1PHIE3cRSKESQ267lGKcHk3YXZvQUDFb95QHgHSiRkEjXtntPkjz0GNVhK8m8TcG5/Ewyj0e6pBF3Ebn2yemmcoVgp7/ZP8o9diC/lNABCjh4EnH1RBoXrsH7/PYrH02g1NZS9aqkcw6Cy87VUdL2DgKMXiuELB04bb6Vd1hisRy/A+/gZ5K1ZRuHMmVTcfjv+Y47BUBTMGzYQ88YbpFx8MelHHEHS5ZfjfPddTFu3NurvR1sUO2kS2s6dBDt1ouKee1B9OTh3hP+NLu/xrzptmNLkLJY9Q/o3grVkWZv/cyEhk6imuFyY8sIzCGQmkxBCRMbuuUza9tqXO1iWLCF19GisS5ag2+2UvPgipS+8gOFwNGWZLZL39NMJpaRgys/HNn8+7rS/oat2zK6NxGU8Wcv8pXMOek1t82ZSzjqL2JdeCgd+48dTsGAB/uOPb4J3JJqMyUTF3XcD4JwyBbWwsPqpih7hbiZ73pdolftuE99Q1uIfAAjEDcD8yx8owSDBbt2q/60QYrc9c5ourJrT9DgJG/6BEopwqLPX0rjYHa9XL43LH/I9ld3uBFPNcN2bMga6gtHOhur1VnfjNTeKy7VnOd9QqOx0LRXd76Lg2B/IH/wdFV1v3ydwSv9pEE7b6wSv6EnRVx+T99tvFL/+Ou4LLyTUrh2qx4Nt4ULiH3qItBNOoN3w4cTfdx+2uXNRKiqi+4ZbOPPq1TjfeQeAsmeewXA6icucjKp78cUPwZtyRpQr3L89c5lUTP5cTJ6MKFcUXRIyiWq7B2CGkpNlhwUhhIiQUNVcpn06mUIhYp99luSLL8ZUUEDgsMMonDMHz9/+FoUqWyiLBffFFwPgmDoVwxxfvXVwzI43quYvnUDBoP8dfP6SruOcMoXUM87A8vvv6AkJFL/5JqWvvCL/T2ylvGecgb9/f1S3m5hXX61+PBB7NJ6UsSgYxGZNjvh9d4dM3r3nMY0cGfH7iFbCZKP0sOf3mtM0k5TVZ0dsTpPm2kLy2ov/sjTuvxQf9R4he+3Bpyd1LCjAwAAAtjlzIlJLpFkXLEDxeiEN/EcPQLdVzdFTFIIxh1PR/e79B04bbiF9aX/id/0TZViIssmPkbd6Nfnz51P24IP4jjsOw2JB27ED54cfknTNNaQfeSTJ559PzMsvY/7tN9D16H4CWhK/n4S770YxDNznn4/vxBPRKtZhz/0UgPKeDzXr0QF75jJpYIC1pG0vmZOQSVTTtm4FZKmcEEJE0u7h33t3Mql5eSRffDGxL7yAYhi4LrmEwv/9j2Dv3tEqs8VyX3opALYff8S0bVv1kjmAys43UnzUhxjmpANeQ921i+RLLiH+4YdRfD68J51E/nff4T3rrEatXUSZolBRNfvD+cEHqNnZ1U9VdL8HAxV74VzM5asjd089iLVkCVA19PvHqtlMo0ZF7h6i9ame0/RJxOY01Vwat+QvS+NGH/C1QefhBO3dUAaHd2GzzZ8PwWC9a2ksey+V8+xvJ9F9AqcF+w2cEtf9HXPyRtzXXkbRp5+S+8cfFL3/PpVXX02we3eUYBDrihXEPfMMqWPGkHbMMSTccgv2zz9HLShoujfeAsW8/jrmjRsJJSVR/sgjYBjEb30cBQN3u3MIxA2MdokHFBg0CEPTUIr8UCBzmSRk+v/27js66ir///jzMzOZzKQTkhBpoReRXq2AsKtgwVVhxcaq2FbR37osll1dXbGha/tiF3dFVxZEQSQCKoogKKi4QoIGaVITEpJJn0z7/P4YGIi09Ank9Tgn58x8yv28P3PuJJP33Pe9EmI7MJJJSSYRkTpzoATGuj/JFLl8ebA8btUqAlFRFDz/PIVPPYWpuX5qxJ+WhnvYMACi/vMffDE92NdrFnl93qOo49/AcozVukwT5/vvkzJyZKhc0fXYY+S/9RaB1NSGuQEJq4pzzqFiyBCMigpin302tN0X3Zny1OCowrgtj9fZ9SKKv8fiLyJgSyBQlETE5s2YFgsVZ5xRZ9eQk5cnYQi5/T+qPE/TjmrO02SaOPZ+eEhpnBd385FHLY07IsOgPOkC6AJmXAQWlwv76tU1v7F6YJSU4Fi6NPhkMJQnX1CFkwx8Md0PTzg5Ox6ecFp/PY7ixXiGDaZo6lT2fvklOatW4Xr0UcrPO49AdDTWffuIev99mt15J6l9+pB03nnEPvYY9lWrwOOp3xfgBGLbtInY54IrehY9/DCBxEQi8z/fn/y0U7x/QYbGzHQ68fbuHXySBXbXV2A23ZFsSjJJiG1LsHZU8zGJiNSd0EimbduInTaNxCuvxJqXh7d792B53GWXhTnCE1/Z1VcDEDV7Nng8VCSegydhyDHPMfLzaXbLLTSbNAlLURGevn3JXbKEsmuvbdRD8qWOHTKaKWr27NDUAbB/qWzDTqRrJfaCFXVyOUf+gVXlziZyRfCbbm/fvirJlCoLOFpVnqdpc9XnaTpYGnfLr0rj3jxqadzRuJNHgRXoG/xH2rF4cU1up944PvkEw+OBU8Bz2iGlclV1aMJp0BdHSDgtqZRwcua8T6BVImUTJlDwxhtkZ2SQN3cuxbffjqdnTwDsGRnETp9O0tixpJ52Gs2uu46of/8b67Ztdf8CnCgCAeL/8hcMjwf3uedSPmYMBHzEbX4YgNLW1+N3tg1zkFVTMXgwAGaWFat3H7bSrDBHFD5KMklIKMmkkUwiInXmwJxMFpcrOJm0aVJ61VXkfvghvk6dwhzdycE9ciT+Fi2w5uVV6R+dyM8/J2XkSJwLF2LabBRNnkze/Pn4O3ZsgGilsfEMGoT73HMxfD5in346tN3vaB1aLjtuyxN1slpQpfmY9k+WrFXlpNoOzNPUaWqV5mkKlsY9Wqk0rjjtriqVxh2NN7YP/shTDpbMLV7cqFbUcixYEHxwrFK5qqpBwsmwVOA5/XSK772XvMWLyf7f/yh4/nnKLr0Uf/PmWEpLcX78MQl//SstzjyTlDPPJP6vfyXy448xSktr/wKcIKLeeovINWsIREdT+PjjYBhEZc8momwjAVsCxW3vCHeIVeYJJZmCK+BFulaFM5ywMkyzEf02OAnk5ubi9XrDHUaNpPbogcXlYu+nn+Lr3j3c4YiInDRa9OqFdd++4IeoadMov+SScId00omdNo3Y556j4swz2TdnzhGPMcrKiJs6leg33wTA26kTruefPzjEXZqsiPXrST7/fEzDIPfTT/F16waAxZNLytdnYAmUkd9jBu7kmq9uZHgLSF3ZC4MA2YNWkzzofKwFBeTNn49n4MC6uhVpYuyur2mWeRNW7z4CtgTyT30JT+L+xKVp4shdSPzmh7BW7AHAnTiCws7/wO9sV+trx/38ADFbZ2DeasVw+8ldtAhvr161bre2jKIiUnv3wvB44XHIHrum+iOZqsI0sZX+hDP3Qxx7FxJRvvngLiMSd+Iw3CkX4m7+G0xb7MHzAgEiMjOJXLaMyGXLsH/7LcYhc1qZERF4Bg6kYtgw3MOG4Tv11JNyhK1l925Shg/HUlKCa+pUyq67DsNXSsrqM7F6cyns9A9KW98Q7jCrzCgsJLVHDwzThBegvON5FPR8I9xh1amIiAiSk5OPe5xGMklQRQUVZ5yBt1s3/O3ahTsaEZGTSsmdd+L+zW/IXbRICaZ6UnbVVZgWC5ErV2LdvPmw/RFr15L829+GEkwlN9xA7uLFSjAJAN6ePSkfPRrDNIl96qnQ9oA9mdLWEwGI3foEmP4aXyOyYAUGAbxRXbFuysdaUEAgJgZPnz61DV+asOA8TYvwxPauNE+TrXQTzdeND5bGVezB52gTLI3rNbNOEkwA7uTRYAd6BxMgjaVkzvHxx8EEUyvw9OhXPwkmOGSE05RDRjjdeewRTr5isFjw9uxJyaRJ7HvvPbIzMsh/4w1Kr7kGX9u2GF4vkatWEffoo6T89re06NePhDvvxDl/Ppb8/Pq5l4ZmmiTcdx+WkhI8/fsHS9UJrgxr9ebic7YLjSQ9UZjx8QcHamRBpOurWv3NOJFpJFMdO5FHMomIiJzIEq+9FsfSpZTcfDNFDzwQ3Oj1Evvcc8Q8/zyG348/NZWCZ57BoxIl+RXbxo0kjxiBEQiQm56Od3/yx/AW0mL1GVh8Lgq6PRuaELy64n/6M9HZ/6Wk9U0EFiUS9/jjlJ93HgVvnFzfdEuY+N0kbLyHqJx3ATAxMDAxjUhK2t5Gcds/Vm1S7+ow/bRY1Q/r53nwEni7diX3s8/q9ho1cOBvAZdC4ZQHKG1zc8MGUNMRTvvPtW7dSuQXX+D4/HPsq1ZhKS8/5HwDb69eVAwbRsWwYXj69QPbMRa4aKQcCxaQeOutmBER5C5Zgq9rVywVe0hZfRaWgJv8Hq8Fk5gnmLj77yfmjTcwz7djXOMht/8ivLHhH91XVzSSSURERJqU0v0TgDvnzAG3G9umTSSNGUPsM89g+P2UXXIJe5cuVYJJjsjXpQvll14KBMsvDzAj4ilp+8fg9m3/hEANVoUyTRz752Oq0HxMUh+sDlzdnqGw08OYWDEwcSeOYO+gzyhu/+e6TzABGFbcSedBXzCtBhFZWVj3z/EaLobLReTy4AT7DAF38oVhCOLXI5w+Oc4Ip3kYvpLQuf4OHSi77jryZ84kOzOTvNmzKbn1Vrzdu2OYJvYffiD2uedI+t3vghOIT5xI1NtvY925s+HvtQaMggLi778fgOI77sDXtSsAcVufxBJwUxE3EHfSqHCGWGOeQYMAMDcG52WyF6wMZzhho5FMdUwjmURERMLE56PFkCFY9+yh/KKLgqsLud0EEhJwPfoo7jFjwh2hNHLWX34h5ZxzMHw+8ubOxXP66QAY/vLgPCGeHFydH6Gs1R+q1a6t5CdSvh2BaXGQ3XcNqT37Y3i95KxYgV8Lrkgds5X8iMVXhCdhcL1fKzL/C5qvuxLziQiMdV4K//Y3Sm+9td6vezTO2bNpdtdd0AY8L/Yjr9+HYYvlMKaJrfRHnLkLce79EFv5wYTcwRFOF+0f4RRzxCYs2dlEfvFF8Gf5cqwFBZX2ezt2PDjK6fTTMZ31kFyspYS77iJq9my8XbqQu3gxREZiK8kk+dvzMDDJ7fch3rh+4Q6zRix795Laty+mAcYr4G5zLvm93gp3WHWmqiOZGmWSafHixXz44Ye4XC7S0tK4/vrr6XSUFXhWr17NvHnzyM7Oxu/3k5qaykUXXcQ5h3wzZJomc+bMYenSpZSWltKtWzcmTpzIKaecEjrmtttuIzc3t1LbV155JZdUc+4MJZlERETCJ+aZZ4g7ZE4d99ChuP75TwKH/M0XOZb4e+8leuZMKgYOZN+8eaEJd6N2vUnCz/fhj0hm75BVmNaoKrcZveNl4jc/jLvZMEr33UDza67B17o1e7/++qSc0FeakICH1FV9sCwqhH+Dp39/8g6s7BYGiVdfjePzz+FyKJwchlK5qqqDhBN+PxHr1gUnEP/iC+xr12L4D84BZEZGUjF4MBVDh1IxbFhwxFCYf9/Yly8nafx4TMMgb/58vAMGgGnSfN14IgtWUJ58MQU9XgprjLWVctZZ2LZuhckQ6B9F9lkbwBIR7rDqxAmbZFq1ahXTp0/nxhtvpHPnzqSnp/P111/z7LPPEh8ff9jxmZmZlJaW0rJlS2w2G2vXrmXmzJncc8899NlfSz9//nzmz5/PbbfdRkpKCrNnz2b79u08/fTT2O3BoWy33XYbw4cPZ+TIkaG2HQ4HDoejWvErySQiIhI+lj17SDn3XPB4KLr/fsomTAj7h2o5sViys2lx5pkYbjf73nqLinPPDe4IeEhZMxSbeztF7e+lJO32KreZ+MN4HAXLKez4d6yv7yLm9dcpveoqCg8pyxM5USX8eCdRP86FScE5g3K++45AixYNHoeRn09q377BldqegpzfrcFfX5N+16XjJZyaD8edfOGxE04EVzeL/PLL4CinZcuw7dpVab8/NRX3sGHBpNPZZ2M2a1Zvt3TE+MrLST73XGzbt1Ny/fUUPfwwAJH7Pqf5+qsxDTt7B32B39m2QeOqa/GTJxM9axaBiyOx/L6C3L7z8cafHCuInrBzMi1cuJARI0YwfPhwWrduzY033ojdbufzzz8/4vE9evRg0KBBtG7dmtTUVEaPHk1aWho//fQTEBzF9NFHH3HppZcycOBA0tLSuP322ykoKOCbb76p1JbT6SQhISH0U90Ek4iIiIRX4JRT2Lt0KXvXrKHsD39QgkmqLZCaSumECQDEPvEEBALBHRY7xe0mAxCz40UMr6tK7Rn+ciJdq4H98zGtWBF8fPbZdRu4SJi4k0dDIpidIjBME8fHH4clDufixcEEUxp4uvY7MRJMsH8Op1Mpbj+FvYOWB+dwansHPmeH4BxOeYtp9uPtpK7sRbOMGyrP4XQIMz4e9wUXUDhtGntXr2bvsmUUPvgg7uHDMR0OrNnZRP/3vyTeeiupvXqRdNFFRL/8MpacnAa5zdinnsK2fTu+li0pvvvu4MaAj7jNwWRTaevrT/gEExwyL9OmYKlipGtVOMMJi0aVZPL5fGzZsoWePXuGtlksFnr27MnGjRuPe75pmqxfv57du3dz6qmnArB3715cLhe9eh2c1T0qKopOnTod1ub8+fO5/vrrmTJlCgsWLMDvP/qSg16vl7KystBP+SGz/ouIiEj4BFq2JNC8ebjDkBNYye23E4iOxp6RgeOjj0Lby1tcgje6GxZfITE7qlbSYXd9jWFW4I88hUBhDBFZWZiGQcVZZ9VX+CINyt3sHAKWKIz+wWoOx5IlYYnD8eH++ZcGQ3k4JvyuCwcSTh3urlXCCcPA17kzpTfeSP7bb7MnI4N977xDyU034e3SBSMQwL52LfEPP0yLAQNIvOYaHAsWgNtdL7cV8cMPRL/6KgCFjz+OGRMckRWVPYeIsiwCtgSK206ql2s3NM+QIQBYsoqgAiKb4OTfjWq9w6KiIgKBAAkJCZW2JyQksHv37qOeV1ZWxs0334zP58NisXDDDTeEkkoulwvgsFK7+Pj40D6AUaNG0b59e2JiYsjKymLWrFkUFBQwYf83Wb82b9485s6dG3revn17nnjiiWrcrYiIiIg0RoHEREpvuonYZ54h9qmncI8aBVYrGFaK208hMeN6onfOoLTVDQQiU47ZVmTBMgDczYYR+eWXAHh7927wUhWRemN1UtH8XJwDFsJsiPzyS4yiIsy4uAYLwbJvH5Er9/8zH65V5eragYTT/lFOttINOPcuxJm7EFv5Fpx5i3HmLa5aSZ3TGSyTGzoU/v53LLt24Vi6lKi5c7F/9x2Ozz7D8dlnBOLjKb/4YsrGjcPbt2/djAb2ekmYPBkjEKDsd7+jYsSI4O35Sond9iQAxe3+hBmRUPtrNQL+Nm3wn3IK1j17YBPYnd+B3w3WplMl1aiSTDXlcDh48skncbvdrF+/npkzZ9KiRQt69OhR5TYuvPDgL6K0tDRsNhuvvfYaV155JRERh0/U9bvf/a7SOYaG44uIiIicNEpuuonof/2LiJ9/xvn++5SPHQuAu/lv8cT1w160lpjtz1PUeeox24nMDy6nXpE4DMfyxcHHhyxQI3IyKE8ejbPlQsxWERi7vDg++4zyai6gVBuOjz4KTnrdHjydT6BSuaoyDHwxPSiO6VH7hBMQaNWKsmuvpezaa7Fu2kTU3LlEzZ2Ldc8eot96i+i33sLbqRPlY8dSdtlltVo8I+aVV4jYsAF/s2YUPfTQwe07XsLq2YvP0Y7SltfWuP1GxzCoGDyYqPnzCWyKxtKjFHvRWjzNzgh3ZA2mUZXLxcXFYbFYKo0wguBopF+PbjqUxWIhNTWVdu3acdFFFzFkyBDmz58PEDqvsLCw0jmFhYXHbLNz5874/f7DVpw7ICIigqioqNCPsxEuDykiIiIiNWPGxVFy220AxP7zn+DxBHcYBkXt7wEgevfbWMu3H7UNi3sXEWU/Y2KhIv6Mg/MxKckkJ5mKxBGYRuTBkrnFixv0+s6ToVSuqg4knEIldR/vL6lrf5SSuvlHLqnbz9+pE8X33EPO6tXkzZpF2aWXEnA4iNi0ibjHHqPFoEEkXnklzvnzoZpTxFg3byb26acBKHrwwVApu6ViD9H7S46LOt4HFnvNXotGyjN4MADmz8FVSCNdTatkrlElmWw2Gx06dCAjIyO0LRAIkJGRQZcuXarcTiAQCK3wlpKSQkJCAuvXrw/tLysrY9OmTcdsc9u2bRiGQVwDDvMUERERkcaj9Lrr8KekYNuxg6hZs0LbPc3OpKLZ2Riml9htTx/1fMf+UUzeuL5Yf96NNS+PQFQUnv796z12kYZk2mKoSDwHBgSfR372Wb3N7/Nrltxc7F99FXwy+CQplauqSgmnFUdJON1WtYST1YrnnHNw/d//kfO//+F66ikqBg/GCARwfPEFzW67jdS+fYmfMgX7N9/A8RapDwRIuPtujIoK3MOGUX7ZZaFdsVufwhJw44kbgDtpdB2+II3DgSSTZYMLfGBvYpN/N6okEwTL1pYuXcqyZcvYuXMnr7/+OhUVFQwbNgyA6dOn884774SOnzdvHuvWrSMnJ4edO3fy4YcfsmLFCs7ev2KHYRiMHj2a999/n2+//Zbt27czffp0mjVrxsCBwaUEN27cSHp6Otu2bSMnJ4cVK1bw5ptvcvbZZxMTc/RlIkVERETk5GU6nRTfcQcAsc89V+lb/AOjmZw572ErPfICNaH5mA5ZVc5z+ulgP7m+tRcBKE8aBe3BbG7DUlp6cI6keuZIT8cIBKAjeDqdhKVyVVWThJP/yCOTzNhYysaPZ9/775OzciXFf/oTvtatsRQXE/2f/5B0ySWknH02Mc8+i3XXriO2ETVrFpFffUXA6aTw8cdD8zvZSjYQlT0bgMKOD5yUq8D6OnfG36wZRoUXtoK96HsMf1m4w2owjW5OpjPOOIOioiLmzJmDy+WiXbt23HfffaHStry8vErzH1VUVPD666+zb98+7HY7rVq1YtKkSZxxxsGaxzFjxlBRUcErr7xCWVkZ3bp147777sO+/w+8zWZj1apVvPvuu3i9XlJSUrjgggsqzbkkIiIiIk1P2ZVXEvPyy9h27iT6zTcpveUWALxxfShPGo0z7yNit06j4LTXK58Y8BFZEJzou6LZUOK+CE5wWzF0aIPGL9JQ3Em/xbRYMfr64NNgydyBSZ7rk3PhwuCDplAqV1VHnMPpw/1zOG0NzeHki2xJUaeHcCeNOmqyx9+uHcWTJ1N8113Yv/qKqHffxZGejm3rVuKefJLYp57Cc+aZlI0di3v0aMyoKCzZ2cRNDc5XV3z33fjbtAm1F7d5KgYm5ckX440/SUd1Wix4Bg3CuWQJ/s3xWDsXYi/8horEpvH73zDN441zk+rIzc0NleqJiIiIyInPOXs2ze66C3+zZuz96ivM2FgAbKUbSf5mBAYBcvul443rEzonovBbkr8fQ8CWQHa/NZxyWk+Migr2LluGr3PnMN2JSP1q/sMVwVF7j4G/eXNyvv8+uDJjPbHk5NCif38M04TnIOfCNU13JFNVmGYo4RSVMxdrxR4guPplYed/4I/qWKVmjNJSHOnpRM2ZQ+SBUkUgEB2N+8ILsezZg2P5cjx9+5L3wQehPhCZv4zm667CNOzsHfQFfmfbOr/FxiL6lVeI/8c/8A85BeukPRS3uY3ijveFO6xaiYiIIDk5+bjHNbpyORERERGRxqT8ssvwduyItaCA6NcPjljyRXehvEVwnpG4rY9XOufAfEwVzc4i8ptvMSoq8Kem4uvUqeECF2lg5UmjoRuY0Vas+/Zh/+67er2eMz09mGDqDJ72fZVgOp5QSd097B20guK2d2AadhwFy0j5ZiSxWx6vUlmXGR1N+bhx7Js7l5yvv6Zo8mR8aWlYSkuJmj0bx/LlmDYbriefPJhkNP3EbX4YgNJW153UCSYAz5AhABiZLgg0rcm/lWQSERERETkWm43iyZMBiHn5ZYz8/NCu4nZ/xjQiiCxYgX1/eRxAZP7nAFQkDiNy+fLg46FDT8r5R0QOcCedj2kzMPr4AXAsWlSv13McuqpcykX1eq2TjWl1BudvGrgUd7NhGKaH2O3/R/KaYThyPzr+xN77+du0oeRPf2LvypXkvf8+pePH409Npehvf8PXvXvouKg9s4ko/YmALYHitDvq6a4aD2+PHgSio7EUl8NOiCheh+ErCndYDUJJJhERERGR43BfeCHeU0/FUlJCzEsvhbb7nW0obXk1AHFbHgfTxPAWEFH8Q/C8ZkOJ/GL/qKZzzmn4wEUaUCAyBU/8QAiur4RjyZIqJyuqy7J7N5Fr1gSfNLVV5eqQP6oD+b3eJr/H6/giW2Gr2EVi5o0krrsaa9mWqjdkGHgGD6bwqafI+e47Sm+88eAuXymx24Lz0hWn/T/MiIQ6votGyGbDMyC43KJ/S3MMAthdX4c5qIahJJOIiIiIyPFYLBRNmQJA9BtvYMnJCe0qSbuTgMWJvfh7HPs+JrJgBQYBvFFdoMhGxI8/AlCxf/VjkZOZO2k09ATTbmD75Rds+/t/XXOmpwcfdAVPmkrlasUwcCePInfQF78qoRtB7JYnjroKXVXF7HgZq2cvPkc7SltNqKOgGz/PoEEAmD8HV6yPdK0KZzgNRkkmEREREZEqqBg5Ek///ljcbmKffz60PWBPprT1RABitzyBI1QqNzQ4CTLgOe00As2bN3zQIg3MnTwKHMBpwRFMjiVL6uU6TpXK1bkjl9A9T/KaoThyF9VoVJqlIpvoHcHRn0Ud7gWLvY6jbrwOzMtkySikqN3dlKWOC3NEDUNJJhERERGRqjAMiu6+G4Co//wH644doV0lbW4hYIsnoiwLZ/Zc4AjzMYk0AX5HazyxvTGClUI462FeJuuuXdi/+w7TAAapVK6uHbmEbiKJ66+pXgkdELv1KSyBcjxxA3AnX1BPETdOnj59MO12LHkuys2L8MWcGu6QGoSSTCIiIiIiVeQ580wqzjoLw+sl9plnQtvNiARK2vwRAIMApsVBRdyg0EgmlcpJU+JOGgX9wLRARGYm1u3b67T9AxN+G93A01alcvXiSCV0+Z9Xq4TOVrKBqOz/AlDY8YGmt/CBw4GnTx8A7KtXhzeWBqQkk4iIiIhINRwYzeR8911smzaFtpe2vh6/PQWAivgh2DZtx5qTQ8DhwDNwYFhiFQmH8uTREAt0CyYV6rpkzrlwYfDBYChP0Sim+nSwhO7TX5XQDcORu/iYJXRxm6diYFKefBHe+P4NF3Qj4hk8GIBIJZlERERERORIvP36Uf7b32IEAsQ+9VRou2mNorDjgwSs0ZS1vDq0qpxnyBBwOMIVrkiD80d1xBvVFaP//nmZFi+us7at27dj//57lco1MH9Ux1+V0O0kMfOGo5bQReYvw1HwBaYREZyLqYk6kGTSSCYRERERETmq4r/8BdMwcH74IbaMjNB2d4sxZJ+9EXfyqIOlcuecE64wRcLGnTwa9g9esa9Zg2Xfvjpp98AoJuNU8LTui9/Ruk7alSoIldAto7jtpMoldFunHSyhM/3EbX4YgNJW1+F3poUx6PDyDBiAabFg++UXLHv2hDucBqEkk4iIiIhINflOPZXyMWMAiJs27fAD3G7sX30FKMkkTVN58ihIBrOdgREIEPnJJ3XSrqPSqnIaxRQOpjWK4g737C+hGxosofvluVAJXdSe2USU/kTAlkBx2h3hDjeszNhYvD16AMFka1OgJJOIiIiISA0U//nPmFYrjqVLifj220r77N9+i8Xtxp+Sgq9btzBFKBI+vuhT8TnaYQwIlsw566Bkzrp1K/Z16zAtwECVyoVbsITuP+T3eA1fZMtQCV38z8HyuOK0OzEjmoU5yvALzcv09ddhjqRhKMkkIiIiIlID/g4dKBs3DoC4xx+vNAFu5PLlwP5V5ZraikoiECqtOlAyF7l8OUZpaa2arFQq10qlco2CYeBOHr1/FbpJmEYEhunD50ijtNWEcEfXKJSNHUv+yy9TfNdd4Q6lQSjJJCIiIiJSQyV/+hOm3U7kV19h3z8HExySZBo6NFyhiYRdefJoaANmCwOjooLIzz+vVXvOBQuCD4aoVK6xOVhCt5TiNn8k/7TXwRIZ7rAaBd9pp+G+6CICycnhDqVBKMkkIiIiIlJD/latKL3mGmD/3EymiWXfPuzr1wP7RzKJNFHe2D74I1NDJXOOJUtq3JZ10yYiNmzAtAIDVCrXWPmjOlLc8a/4Yk4NdygSJkoyiYiIiIjUQsmkSQScTuzff0/kJ59g//JLALzduxNISQlzdCJhZFiCo5n2l8w5Pv0UPJ4aNRUqlesBnpYqlRNprJRkEhERERGphUByMqU33AAERzM59pcEaVU5EXAnjYLOYMYbWIqKajz5sfPAqnIqlRNp1JRkEhERERGppZJbbyUQF0fEjz/ifP99QPMxiQB4Egbjj2yO0W9/ydyiRdVuw7ZxIxE//aRSOZETgJJMIiIiIiK1ZCYkUHLzzQAYfj9mZCQVgwaFOSqRRsCw4k46HwYEnzo+/hgCgWo14ThQKtcTPKkqlRNpzJRkEhERERGpA6UTJ+Jv3hwAz6BB4HSGOSKRxsGdNAp6gOk0sGZnE/G//1X9ZNPUqnIiJxAlmURERERE6oAZE0PRX/+KabVSOn58uMMRaTQqmp1JwBGH0Xt/ydzixVU+15aVRcTPP2PagP4qlRNp7JRkEhERERGpI+W//z17tm7FPWZMuEMRaTwsdtxJIw+WzFUjyXRgFJPRCzwtVCon0tgpySQiIiIiUpes1nBHINLouJMugN5g2iBi82ZsP/98/JNMU6vKiZxglGQSERERERGRelWROJRAjBOjR/B5VUYz2TZswLZlC2YE0E+lciInAiWZREREREREpF6ZVicVzc+tVslcqFSuN3hSVConciJQkklERERERETqnTtpNPQH0wD7//6HZffuox9smjgXLgw+VqmcyAlDSSYRERERERGpd+7mIzAT7Bidg88dH3981GMjMjKwbduGaQf6qlRO5EShJJOIiIiIiIjUO9MWS0Wzs0Mlc85Fi456rONAqVwf8CSrVE7kRKEkk4iIiIiIiDSI8uQLoH/wsf2rrzBcrsMP0qpyIicsJZlERERERESkQbiTfoOZaoXWYPj9OD799LBjIn74AduOHZiRQB+VyomcSJRkEhERERERkQZhRiTiSTgdBgafO5YsOeyY0KpyfcGTpFI5kROJkkwiIiIiIiLSYMqTR4VK5iI//xzKyw/uNE0ch5bKaRSTyAlFSSYRERERERFpMO6kUZjtgCSwlJcTuWJFaF/Ed99h270b0wH0BrfmYxI5oSjJJCIiIiIiIg0mENkCT/zAI64yd2DCb6MfeJqrVE7kRKMkk4iIiIiIiDQo96Elc598Aj4fBAI4Fy4MblSpnMgJSUkmERERERERaVDupNHQFcwYsBYUYF+zBvu332LNzsZ0Ar1UKidyIrKFOwARERERERFpWvzONnjie2Lvtx6Wg2PxYjBNAIwB4ElUqZzIiUhJJhEREREREWlw7uTR2AfsTzItWoTh9wd3DFapnMiJSuVyIiIiIiIi0uDcSaOhJ5iRYNu9G2tODmYU0BPcyReEOzwRqQElmURERERERKTB+aI74U3ogtHr4DZjIHia9cXvbBO+wESkxpRkEhERERERkbBwJ4+CAYdsUKmcyAlNSSYREREREREJi/Kk0dAXzDgwWwA9VConciLTxN8iIiIiIiISFr6YHviat8U2bTtYVConcqLTSCYREREREREJD8PAnTwaYoFolcqJnOiUZBIREREREZGwKU8aFXqsUjmRE5vK5URERERERCRsvHH9KW57OwFbgkrlRE5whmmaZriDOJnk5ubi9XrDHYaIiIiIiIiISJ2IiIggOTn5uMepXE5ERERERERERGpNSSYREREREREREak1JZlERERERERERKTWlGQSEREREREREZFaU5JJRERERERERERqTUkmERERERERERGpNSWZRERERERERESk1pRkEhERERERERGRWlOSSUREREREREREak1JJhERERERERERqTUlmUREREREREREpNaUZBIRERERERERkVpTkklERERERERERGpNSSYREREREREREak1JZlERERERERERKTWlGQSEREREREREZFaU5JJRERERERERERqTUkmERERERERERGpNSWZRERERERERESk1pRkEhERERERERGRWlOSSUREREREREREak1JJhERERERERERqTUlmUREREREREREpNaUZBIRERERERERkVpTkklERERERERERGrNFu4ATjY2m15SERERERERETl5VDXXYZimadZzLCIiIiIiIiIicpJTuZxImJSXl3P33XdTXl4e7lCkCVL/k3BTHxRpXPSelHBTH5RwUx+sG0oyiYSJaZps3boVDSaUcFD/k3BTHxRpXPSelHBTH5RwUx+sG0oyiYiIiIiIiIhIrSnJJCIiIiIiIiIitaYkk0iYREREcPnllxMRERHuUKQJUv+TcFMfFGlc9J6UcFMflHBTH6wbWl1ORERERERERERqTSOZRERERERERESk1pRkEhERERERERGRWlOSSUREREREREREak1JJhERERERERERqTVbuAMQqW/z5s1jzZo17Nq1C7vdTpcuXbj66qtp2bJl6BiPx8PMmTNZtWoVXq+X3r17M3HiRBISEgDYtm0b8+fPJysri6KiIlJSUvjNb37D6NGjQ21kZmby0EMPHXb9V199NdTOkaxevZpPPvmELVu2UFJSwrRp02jXrl1of0lJCXPmzOGHH34gLy+PuLg4Bg4cyBVXXEFUVNQx7/2XX35hxowZbN68mbi4OM4//3zGjBlT6Zj09HQ+/vjjUNuDBw/myiuvxG63H7NtqZqm2v88Hg+vvfYaW7ZsYdeuXfTr148pU6ZUOqamMUv1NNU+mJmZSXp6Ops2baK8vJzU1FQuvvhizj777NAxn376KcuXL2fHjh0AdOjQgfHjx9OpU6fjvawiNdZQ70kAr9fL3LlzWbFiBS6Xi2bNmnHZZZdx7rnnHjPGxYsX8+GHH+JyuUhLS+P666+v9L749NNP+fLLL9m6dSvl5eX861//Ijo6+rj3npeXx2uvvUZmZiYOh4OhQ4dy5ZVXYrVaASgoKGDmzJls2bKF7OxsRo0axR/+8IcqvrJSVeqDR++DACtWrGDBggXs2bOHqKgo+vTpwzXXXENsbGxVXl45jqbc/9544w2ysrLYsWMHrVq14sknn6y0f+/evdx+++2HnTd16lS6dOly3PYbCyWZ5KS3YcMGzjvvPDp27Ijf72fWrFlMnTqVp59+GofDAcCbb77J2rVrueuuu4iKimLGjBn885//5OGHHwZgy5YtxMfHM2nSJJo3b05WVhavvvoqFouF888/v9L1nn322Ur/+MTFxR0zvoqKCrp168bpp5/OK6+8ctj+/Px88vPzueaaa2jdunXoj2NBQQF//vOfj9puWVkZU6dOpWfPntx4441s376dl156iejoaEaOHAnAl19+yTvvvMOtt95Kly5d2LNnDy+++CKGYTBhwoSqvcByTE21/wUCAex2O6NGjWL16tXHjKG6MUv1NNU+mJWVRdu2bRkzZgzx8fGsXbuW6dOnExUVRf/+/UOvzZlnnknXrl2JiIjggw8+CL02iYmJVXuBRaqpId+TzzzzDIWFhdxyyy2kpqbicrkIBALHjG/VqlXMnDmTG2+8kc6dO5Oens4jjzzCs88+S3x8PBB83/bp04c+ffrwzjvvVOm+A4EAjz32GAkJCUydOpWCggKmT5+O1WrlyiuvBIL/EMbFxXHppZeSnp5e7ddWqkZ98Oh98KeffmL69OlMmDCBAQMGkJ+fz2uvvcYrr7zC5MmTq/1ay+Gaav87YPjw4WzatIlffvnlqMfcf//9tGnTJvQ8JiamWtcIO1OkiSksLDTHjh1rZmZmmqZpmqWlpeYVV1xhfvXVV6Fjdu7caY4dO9bMyso6ajuvvfaa+eCDD4aeZ2RkmGPHjjVLSkpqFFdOTo45duxYc+vWrcc9dtWqVeb48eNNn8931GOWLFli/uEPfzC9Xm9o29tvv23eeeedoeevv/66+dBDD1U678033zT/9re/VTt+qZqm0v8ONX36dPOJJ544bHttY5aaaYp98IBHH33UfOGFF4663+/3m9dee625bNmyarUrUhv19Z78/vvvzQkTJpjFxcXViufee+81X3/99dBzv99v3nTTTea8efMOO7Y67/u1a9ea48aNMwsKCkLblixZYl577bWVPqsc8Pe//93817/+Va3YpWbUBw/2wQ8++MC8/fbbK5330UcfmTfffHO17kGqrqn0v0PNnj3bnDx58mHbq/NZqDHTnEzS5JSVlQEHM8JbtmzB7/fTs2fP0DGtWrUiKSmJjRs3HrOdI2WVp0yZwk033cTDDz/MTz/9VMfRH7y20+msNLT31zZu3Ej37t2x2Q4OWOzduze7d++mpKQEgK5du7JlyxY2bdoEQE5ODt9//z19+/atl7il6fS/6miImOWgptwHjxbzARUVFfh8vhPvG0M5odXXe/Lbb7+lY8eOfPDBB9x8883ceeedzJw5E4/Hc9Q2fD4fW7ZsqXRti8VCz549j3ntqti4cSNt27atVD7bp08fysvLQyWrEh7qgwf7YJcuXcjLy2Pt2rWYponL5eLrr7/WZ+N61FT6X3U88cQTTJw4kfvvv59vv/22wa5bV1QuJ01KIBDg3//+N127dqVt27YAuFwubDbbYXW08fHxuFyuI7aTlZXFV199xT333BPa1qxZM2688UY6duyI1+tl6dKlPPTQQzzyyCN06NChzu6hqKiI9957L1TydjQul4uUlJRK2w78UXW5XMTExHDWWWdRVFTE/fffD4Df7+c3v/kNl156aZ3FKwc1pf5XFQ0VsxzUlPvgqlWr2Lx5MzfddNNRj/nPf/5DYmJipQ+XIvWpPt+TOTk5/PTTT0RERPCXv/yFoqIiZsyYQUlJCX/84x+P2E5RURGBQOCwedQSEhLYvXt3zW90/339ut0DpSdHuy+pf+qDlftgt27duOOOO3j22Wfxer34/X769+/PDTfcUKtry5E1pf5XFQ6Hg2uvvZauXbtiGAarV6/mySef5C9/+QsDBgyo9+vXFSWZpEmZMWMGO3bs4B//+EeN29i+fTvTpk3j8ssvp3fv3qHtLVu2rDRhXdeuXcnJySE9PZ1JkyaxYsUKXn311dD+++67j+7du1fr2mVlZTz++OO0bt2asWPHhrbfdddd5ObmAtC9e3fuu+++KrWXmZnJvHnzmDhxIp07dyY7O5t//etfzJ07l8svv7xascnxqf9VdryYpe411T6YkZHBSy+9xM0331xpjoNDzZ8/n5UrV/Lggw9q4QNpMPX5njRNE4A77rgjNE+a1+vl6aefZuLEiWzevJlHH300dPxNN91Ejx49ahzHoR599FF+/PFHAJKTk3n66afrpF2pe+qDle3cuZN///vfoXspKCjg7bff5rXXXuPWW2+tk9jkIPW/yuLi4rjwwgtDzzt16kRBQQELFixQkkmkMZoxYwZr167loYceonnz5qHtCQkJ+Hw+SktLK2XMCwsLD8ti79y5k4cffpiRI0dy2WWXHfeanTp1CpWLDBgwgM6dO4f2VXdS2fLych599FGcTieTJ0+uVAZ377334vf7AUL/HCUkJByW7T/w/MB9zZ49m3POOYcRI0YA0LZtW9xuN6+++iqXXnopFosqautKU+t/NXVozFK3mmof3LBhA0888QQTJkxg6NChR2x7wYIFzJ8/n/vvv5+0tLRqxSVSU/X9nkxISCAxMbHSRPytWrXCNE327dtHx44dK61sFB8fT0REBBaL5YifH6qz6uctt9wSKkk5UNaakJAQKs8/9J4O7JOGpz54eB+cN28eXbt25eKLLwYgLS0Nh8PBAw88wBVXXEGzZs2qHIMcW1PrfzXVqVMn1q1bV6s2Gpr+g5STnmmazJgxgzVr1vDAAw8cVkLWoUMHrFYr69evD23bvXs3eXl5lZaK3LFjBw899BBDhw5l/PjxVbr2tm3bQn+MnE4nqampoZ/q/DN+YKU4m83GlClTDjs3OTk51O6Bf9y6dOnCjz/+iM/nCx23bt06WrZsGapXrqiowDCMSm0psVS3mmr/q6lDY5a60ZT7YGZmJo899hhXXXXVUcvrPvjgA9577z3uu+8+OnbsWOWYRGqqod6T3bp1o6CgALfbHdq2Z88eDMOgefPm2O32Su9Jp9OJzWajQ4cOZGRkhM4JBAJkZGRUa/nsxMTEULvJyclA8HPJ9u3bQ//UQ/BzidPppHXr1lVuW2pPffDoffBYn40PjIyR2mmq/a+mTsTPxhrJJCe9GTNm8OWXXzJlyhScTmcoMx0VFYXdbicqKopzzz2XmTNnEhMTQ1RUFG+88QZdunQJ/TLZvn07//jHP+jduzcXXnhhqA2LxRJanjs9PZ2UlBTatGmDx+Phs88+IyMjg7/97W/HjK+kpIS8vDzy8/MBQvW+CQkJJCQkUFZWxiOPPEJFRQWTJk2ivLyc8vJyIDik8mhJobPOOot3332Xl19+mTFjxrBjxw4WLVrEhAkTQsf079+f9PR02rdvHyqXmz17Nv3791eyqY401f4HwW+XfD4fJSUluN1utm3bBkC7du1qFbNUT1PtgxkZGTzxxBOMGjWKIUOGhGK22WyhRPv8+fOZM2cOd9xxBykpKaFjHA5HaBllkbrWUO/Js846i/fee48XX3yRcePGUVRUxNtvv83w4cOPmeS98MILeeGFF+jQoQOdOnXio48+oqKigmHDhoWOcblcuFwusrOzQ/E4nU6SkpKOOnF+7969ad26NdOnT+eqq67C5XLx3//+l/POO4+IiIjQcQf+VrjdboqKiti2bRs2m02JqDqkPnj0PjhgwABeeeUVPv7441C53JtvvkmnTp1q/UWaBDXV/geQnZ2N2+3G5XLh8XhCv+9at26NzWZj2bJl2Gw22rdvD8Dq1av5/PPPueWWW2ryUoeNYSolKye5cePGHXH7H//4x9AvC4/Hw8yZM1m5ciU+n4/evXszceLE0LDIOXPmMHfu3MPaSE5O5oUXXgCC34Z/+umn5OfnExkZSVpaGpdddhmnnXbaMeNbtmwZL7744mHbL7/8csaNG0dmZiYPPfTQEc+dPn36Ydn/Q/3yyy/MmDGDzZs3Exsby/nnn88ll1wS2u/3+3n//fdZvnw5+fn5xMXF0b9/f8aPH3/YZHtSM025/912222heXIONWfOnFrFLNXTVPvgCy+8wBdffHHY9lNPPZUHH3wQOHofPXBtkfrQUO9JgF27dvHGG2+QlZVFbGwsp59+OldcccVxRxIuXryYBQsW4HK5aNeuHdddd12lctejXf/QeziS3NxcXn/9dTIzM4mMjGTo0KFcddVVlcpJjvT6/Pq+pHbUB4/dBxctWsQnn3zC3r17iY6OpkePHlx99dVKMtWRptz/HnzwQTZs2HDY9gOfZ5YtW8YHH3xAXl4eFouFVq1acfHFFzNkyJBjxtvYKMkkIiIiIiIiIiK1pnoYERERERERERGpNSWZRERERERERESk1pRkEhERERERERGRWlOSSUREREREREREak1JJhERERERERERqTUlmUREREREREREpNaUZBIRERERERERkVpTkklERERERERERGpNSSYREREREREREak1W7gDEBEREWlKli1bxosvvhh6HhERQUxMDG3btqVv374MHz4cp9NZ7XazsrL44YcfuOCCC4iOjq7LkEVERESqREkmERERkTAYN24cKSkp+P1+XC4XGzZs4M033yQ9PZ0pU6aQlpZWrfaysrKYO3cuw4YNU5JJREREwkJJJhEREZEw6Nu3Lx07dgw9/93vfkdGRgaPP/4406ZN45lnnsFut4cxQhEREZHqUZJJREREpJE47bTTuOyyy5g1axbLly9n5MiR/PLLLyxcuJAff/yRgoICoqKi6Nu3L9dccw2xsbEAzJkzh7lz5wJw++23h9qbPn06KSkpACxfvpz09HR27tyJ3W6nd+/eXH311SQlJTX8jYqIiMhJSUkmERERkUbknHPOYdasWaxbt46RI0eybt069u7dy7Bhw0hISGDnzp18+umn7Ny5k0ceeQTDMBg8eDB79uxh5cqVTJgwIZR8iouLA+D9999n9uzZnH766YwYMYKioiIWLVrE3//+d6ZNm6byOhEREakTSjKJiIiINCLNmzcnKiqKnJwcAM477zwuuuiiSsd07tyZ5557jp9++onu3buTlpZG+/btWblyJQMHDgyNXgLIzc1lzpw5/P73v+fSSy8NbR80aBB33303S5YsqbRdREREpKYs4Q5ARERERCpzOByUl5cDVJqXyePxUFRUROfOnQHYunXrcdtavXo1pmlyxhlnUFRUFPpJSEggNTWVzMzM+rkJERERaXI0kklERESkkXG73cTHxwNQUlLCu+++y6pVqygsLKx0XFlZ2XHbys7OxjRN7rjjjiPut9n0cVBERETqhj5ViIiIiDQi+/bto6ysjBYtWgDwzDPPkJWVxcUXX0y7du1wOBwEAgEeffRRAoHAcdsLBAIYhsG9996LxXL4IHaHw1Hn9yAiIiJNk5JMIiIiIo3I8uXLAejTpw8lJSWsX7+ecePGcfnll4eO2bNnz2HnGYZxxPZSU1MxTZOUlBRatmxZP0GLiIiIoDmZRERERBqNjIwM3nvvPVJSUjjrrLNCI49M06x0XHp6+mHnRkZGAoeX0A0aNAiLxcLcuXMPa8c0TYqLi+vyFkRERKQJ00gmERERkTD4/vvv2bVrF4FAAJfLRWZmJuvWrSMpKYkpU6Zgt9ux2+10796dBQsW4Pf7SUxM5IcffmDv3r2HtdehQwcAZs2axZlnnonVaqV///6kpqZyxRVX8M4775Cbm8vAgQNxOBzs3buXb775hhEjRnDxxRc39O2LiIjIScgwf/2VloiIiIjUm2XLlvHiiy+GnttsNmJiYmjbti39+vVj+PDhOJ3O0P78/HzeeOMNMjMzMU2TXr16cd1113HzzTdz+eWXM27cuNCx7733Hp988gkFBQWYpsn06dNJSUkBgqvMpaenh1akS0pK4rTTTmPUqFEqoxMREZE6oSSTiIiIiIiIiIjUmuZkEhERERERERGRWlOSSUREREREREREak1JJhERERERERERqTUlmUREREREREREpNaUZBIRERERERERkVpTkklERERERERERGpNSSYREREREREREak1JZlERERERERERKTWlGQSEREREREREZFaU5JJRERERERERERqTUkmERERERERERGpNSWZRERERERERESk1v4/7GS5tux0sG0AAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "bs_discrete_ts = bs_discrete.timeseries\n", + "bs_cont_ts = bs_cont.timeseries\n", + "crr_discrete_ts = crr_discrete.timeseries\n", + "crr_cont_ts = crr_cont.timeseries\n", + "\n", + "import matplotlib.pyplot as plt\n", + "plt.figure(figsize=(14, 8))\n", + "plt.plot(bs_discrete_ts.index, bs_discrete_ts.values, label=\"BSM Discrete Div\", color=\"blue\")\n", + "plt.plot(bs_cont_ts.index, bs_cont_ts.values, label=\"BSM Continuous Div\", color=\"orange\")\n", + "plt.plot(crr_discrete_ts.index, crr_discrete_ts.values, label=\"CRR Discrete Div\", color=\"green\")\n", + "plt.plot(crr_cont_ts.index, crr_cont_ts.values, label=\"CRR Continuous Div\", color=\"red\")\n", + "plt.title(\"Implied Volatility Timeseries Comparison\")\n", + "plt.xlabel(\"Date\")\n", + "plt.ylabel(\"Implied Volatility\")\n", + "plt.legend()\n", + "plt.grid()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7fe20da9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2025-07-14 0.499913\n", + "2025-07-15 0.491289\n", + "2025-07-16 0.495288\n", + "2025-07-17 0.496413\n", + "2025-07-18 0.497163\n", + " ... \n", + "2026-01-12 0.537031\n", + "2026-01-13 0.542780\n", + "2026-01-14 0.547029\n", + "2026-01-15 0.552154\n", + "2026-01-16 0.555278\n", + "Length: 131, dtype: float64" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bs_discrete_ts\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db8a9166", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "17.053284342165583" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "crr_binomial_pricing(\n", + " K = strike,\n", + " american = False,\n", + " T = 0.5,\n", + " sigma = 0.25,\n", + " r = 0.03,\n", + " S0 = 220.0,\n", + " dividends = [(0.1, 5.0), (0.3, 5.0)],\n", + " option_type = \"c\",\n", + " dividend_type = \"discrete\",\n", + " N = 200\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a8a168d", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'discrete_vol' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[20], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mmatplotlib\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mpyplot\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mplt\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mdiscrete_vol\u001b[49m\u001b[38;5;241m.\u001b[39mplot(label\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mDiscrete Dividends IV\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 3\u001b[0m cont_vol\u001b[38;5;241m.\u001b[39mplot(label\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mContinuous Dividends IV\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 4\u001b[0m plt\u001b[38;5;241m.\u001b[39mlegend()\n", + "\u001b[0;31mNameError\u001b[0m: name 'discrete_vol' is not defined" + ] + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "discrete_vol.plot(label=\"Discrete Dividends IV\")\n", + "cont_vol.plot(label=\"Continuous Dividends IV\")\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dd470cf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:COST|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2028-03-17|method:CONSTANT|undo_adjust:1']" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DividendDataManager.INSTANCES['AAPL'].cache.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c25bf2fe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2025-12-04 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-05 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-08 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-09 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-10 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-11 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-12 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-15 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-16 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-17 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-18 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-19 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-22 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-23 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-24 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-26 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-29 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-30 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2025-12-31 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-02 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-05 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-06 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-07 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-08 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-09 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-12 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-13 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-14 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-15 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "2026-01-16 ((2026-02-10, 0.26), (2026-05-10, 0.26), (2026...\n", + "Name: dividend_schedule, dtype: object" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vol_data.dividend.daily_discrete_dividends" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ced08f1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[datetime\n", + " 2026-01-14 278.430300\n", + " 2025-12-31 291.604396\n", + " 2025-12-16 294.998661\n", + " 2025-12-17 291.956677\n", + " 2025-12-09 298.578277\n", + " 2026-01-09 277.642885\n", + " 2026-01-15 276.542497\n", + " 2025-12-30 292.907049\n", + " 2025-12-11 299.005601\n", + " 2025-12-10 300.019099\n", + " 2026-01-12 278.633878\n", + " 2025-12-12 298.954905\n", + " 2025-12-15 294.439273\n", + " 2025-12-23 292.374288\n", + " 2025-12-04 302.413885\n", + " 2026-01-08 277.276901\n", + " 2025-12-18 292.168657\n", + " 2026-01-07 278.745903\n", + " 2026-01-05 286.286530\n", + " 2025-12-22 290.774190\n", + " 2025-12-24 293.967672\n", + " 2026-01-16 273.572360\n", + " 2026-01-02 290.538022\n", + " 2025-12-05 300.236797\n", + " 2025-12-19 293.741950\n", + " 2026-01-13 279.635458\n", + " 2025-12-26 293.387536\n", + " 2025-12-08 299.283696\n", + " 2026-01-06 280.996729\n", + " 2025-12-29 293.658153\n", + " dtype: float64,\n", + " datetime\n", + " 2026-01-14 0.03560\n", + " 2025-12-31 0.03547\n", + " 2025-12-16 0.03545\n", + " 2025-12-17 0.03543\n", + " 2025-12-09 0.03632\n", + " 2026-01-09 0.03513\n", + " 2026-01-15 0.03565\n", + " 2025-12-30 0.03540\n", + " 2025-12-11 0.03568\n", + " 2025-12-10 0.03593\n", + " 2026-01-12 0.03533\n", + " 2025-12-12 0.03525\n", + " 2025-12-15 0.03538\n", + " 2025-12-23 0.03547\n", + " 2025-12-04 0.03612\n", + " 2026-01-08 0.03507\n", + " 2025-12-18 0.03522\n", + " 2026-01-07 0.03515\n", + " 2026-01-05 0.03515\n", + " 2025-12-22 0.03528\n", + " 2025-12-24 0.03555\n", + " 2026-01-16 0.03557\n", + " 2026-01-02 0.03533\n", + " 2025-12-05 0.03603\n", + " 2025-12-19 0.03522\n", + " 2026-01-13 0.03560\n", + " 2025-12-26 0.03543\n", + " 2025-12-08 0.03618\n", + " 2026-01-06 0.03520\n", + " 2025-12-29 0.03538\n", + " Name: annualized, dtype: float64,\n", + " open high low close volume bid_size closebid \\\n", + " datetime \n", + " 2026-01-14 85.49 85.49 85.49 85.49 10 9 86.55 \n", + " 2025-12-31 98.90 98.90 97.62 98.03 15 12 96.80 \n", + " 2025-12-16 99.15 99.86 99.15 99.86 21 15 100.10 \n", + " 2025-12-17 99.51 100.15 99.51 100.09 4 8 96.50 \n", + " 2025-12-09 104.55 105.50 104.55 105.32 9 400 101.50 \n", + " 2026-01-09 86.80 87.25 85.30 87.25 15 10 85.85 \n", + " 2026-01-15 87.25 87.25 85.85 86.55 3 14 85.05 \n", + " 2025-12-30 98.00 98.90 98.00 98.90 9 101 97.55 \n", + " 2025-12-11 104.14 104.14 104.14 104.14 1 545 102.00 \n", + " 2025-12-10 104.92 104.92 104.92 104.92 6 392 102.70 \n", + " 2026-01-12 87.21 87.75 87.21 87.75 3 31 86.80 \n", + " 2025-12-12 104.05 104.20 104.00 104.20 3 153 102.00 \n", + " 2025-12-15 100.29 102.05 100.29 102.05 89 17 99.85 \n", + " 2025-12-23 95.50 97.84 95.50 97.55 13 83 97.20 \n", + " 2025-12-04 107.27 107.27 106.50 106.50 2 331 104.50 \n", + " 2026-01-08 85.00 85.20 84.00 85.16 22 317 84.00 \n", + " 2025-12-18 96.00 98.50 96.00 98.50 11 15 97.85 \n", + " 2026-01-07 88.55 89.10 87.00 87.00 39 11 87.00 \n", + " 2026-01-05 94.80 94.80 93.00 93.00 19 13 92.80 \n", + " 2025-12-22 98.10 98.60 97.10 97.10 12 12 96.40 \n", + " 2025-12-24 0.00 0.00 0.00 0.00 0 33 98.80 \n", + " 2026-01-16 83.90 84.10 83.90 84.10 2 10 82.75 \n", + " 2026-01-02 96.20 96.92 96.19 96.92 48 37 95.90 \n", + " 2025-12-05 105.06 106.10 105.06 106.10 2 153 103.00 \n", + " 2025-12-19 97.69 99.13 97.69 99.13 2 74 97.55 \n", + " 2026-01-13 0.00 0.00 0.00 0.00 0 7 87.35 \n", + " 2025-12-26 100.07 100.07 100.07 100.07 32 5 98.10 \n", + " 2025-12-08 104.91 105.00 103.69 103.69 53 311 102.60 \n", + " 2026-01-06 90.00 90.55 89.40 89.54 51 35 88.30 \n", + " 2025-12-29 99.39 99.39 98.32 99.25 52 112 98.25 \n", + " \n", + " ask_size closeask midpoint weighted_midpoint \n", + " datetime \n", + " 2026-01-14 15 87.70 87.125 87.268750 \n", + " 2025-12-31 10 98.80 97.800 97.709091 \n", + " 2025-12-16 30 102.05 101.075 101.400000 \n", + " 2025-12-17 11 101.00 98.750 99.105263 \n", + " 2025-12-09 399 105.90 103.700 103.697247 \n", + " 2026-01-09 15 87.40 86.625 86.780000 \n", + " 2026-01-15 41 86.85 85.950 86.391818 \n", + " 2025-12-30 56 99.35 98.450 98.192038 \n", + " 2025-12-11 544 106.50 104.250 104.247934 \n", + " 2025-12-10 388 107.50 105.100 105.087692 \n", + " 2026-01-12 29 87.60 87.200 87.186667 \n", + " 2025-12-12 154 106.50 104.250 104.257329 \n", + " 2025-12-15 134 102.20 101.025 101.935430 \n", + " 2025-12-23 10 99.20 98.200 97.415054 \n", + " 2025-12-04 10 107.90 106.200 104.599707 \n", + " 2026-01-08 18 88.00 86.000 84.214925 \n", + " 2025-12-18 18 100.15 99.000 99.104545 \n", + " 2026-01-07 29 88.00 87.500 87.725000 \n", + " 2026-01-05 2 94.05 93.425 92.966667 \n", + " 2025-12-22 26 98.10 97.250 97.563158 \n", + " 2025-12-24 21 100.25 99.525 99.363889 \n", + " 2026-01-16 49 83.65 83.200 83.497458 \n", + " 2026-01-02 52 97.95 96.925 97.097753 \n", + " 2025-12-05 262 107.50 105.250 105.840964 \n", + " 2025-12-19 20 101.25 99.400 98.337234 \n", + " 2026-01-13 16 88.35 87.850 88.045652 \n", + " 2025-12-26 27 100.30 99.200 99.956250 \n", + " 2025-12-08 22 106.00 104.300 102.824625 \n", + " 2026-01-06 31 90.05 89.175 89.121970 \n", + " 2025-12-29 14 100.30 99.275 98.477778 ]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sync_date_index(vol_data.forward.daily_discrete_forward, \n", + " vol_data.rates.daily_risk_free_rates,\n", + " vol_data.option_spot.daily_option_spot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cb57546", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'date_range_packet': 0.00013685226440429688,\n", + " 'dividend_load_time': 5.028839826583862,\n", + " 'rates_load_time': 0.022665023803710938,\n", + " 'forward_load_time': 0.2790851593017578,\n", + " 'option_spot_load_time': 0.020485877990722656}" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "vol_data.time_to_load\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39ec8064", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ,\n", + " ]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dividends_ts = vol_data.dividend.daily_discrete_dividends\n", + "dividends_ts = vector_convert_to_time_frac(\n", + " schedules=dividends_ts,\n", + " valuation_dates=vol_data.option_spot.daily_option_spot.midpoint.index,\n", + " end_dates=[to_datetime(expiration)] * len(vol_data.option_spot.daily_option_spot.midpoint.index)\n", + ")\n", + "dividends_ts" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b58a897b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:COST|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2028-03-17|method:CONSTANT|undo_adjust:1',\n", + " 'symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-07-17|method:CONSTANT|undo_adjust:1',\n", + " 'symbol:NVDA|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:NVDA|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-07-17|method:CONSTANT|undo_adjust:1',\n", + " 'symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE',\n", + " 'symbol:AMD|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-07-17|method:CONSTANT|undo_adjust:1',\n", + " 'symbol:AMD|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2027-12-17|method:CONSTANT|undo_adjust:1']" + ] + }, + "execution_count": 218, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DividendDataManager(\"AMD\").cache.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "474046f9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using non-batch processor for CRR implied volatility estimation.\n" + ] + } + ], + "source": [ + "\n", + "v = vector_crr_iv_estimation(\n", + " S0.tolist(),\n", + " K,\n", + " T,\n", + " r.tolist(),\n", + " market_price.tolist(),\n", + " dividends_res,\n", + " right,\n", + " [100] * len(dividends.daily_discrete_dividends),\n", + " dividend_type,\n", + " [True] * len(dividends.daily_discrete_dividends)\n", + ")" + ] + }, + { + "cell_type": "raw", + "id": "7cdf6154", + "metadata": {}, + "source": [ + "ts_start = \"2025-01-01\"\n", + "ts_end = \"2026-01-18\"\n", + "expiration = \"2030-10-13\"\n", + "_right = \"p\"\n", + "req = LoadRequest(\n", + " symbol = \"SBUX\",\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration\n", + ")\n", + "\n", + "model_data = _load_model_data_timeseries(req)\n", + "model_data.forward" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bfbf391", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Timestamp('2025-12-04 00:00:00'), datetime.datetime(2026, 1, 18, 0, 0))" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from trade.datamanager.utils.date import _sync_date\n", + "\n", + "_sync_date(\n", + " symbol=\"AAPL\",\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2026-01-18\",\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5086135a", + "metadata": {}, + "outputs": [], + "source": [ + "import yfinance as yf\n", + "import pandas as pd\n", + "from trade.helpers.helper import to_datetime\n", + "\n", + "start_date = \"2023-01-01\"\n", + "end_date = \"2024-01-01\"\n", + "interval = \"1d\"\n", + "start_date = to_datetime(start_date) - pd.Timedelta(days=5)\n", + "end_date = to_datetime(end_date) + pd.Timedelta(days=5)\n", + "def deannualize(annual_rate: float, periods: int = 365) -> float:\n", + " \"\"\"Converts annualized interest rate to per-period rate.\n", + "\n", + " Uses compound interest formula to convert annual rate to daily rate\n", + " or other period-based rate.\n", + "\n", + " Args:\n", + " annual_rate: Annualized interest rate (e.g., 0.05 for 5%).\n", + " periods: Number of periods per year. Defaults to 365 for daily rate.\n", + "\n", + " Returns:\n", + " Per-period interest rate (e.g., daily rate if periods=365).\n", + "\n", + " Examples:\n", + " >>> # Convert 5% annual to daily rate\n", + " >>> daily_rate = deannualize(0.05, periods=365)\n", + " >>> print(f\"{daily_rate:.6f}\")\n", + " 0.000134\n", + "\n", + " >>> # Convert 5% annual to weekly rate\n", + " >>> weekly_rate = deannualize(0.05, periods=52)\n", + " >>> print(f\"{weekly_rate:.6f}\")\n", + " 0.000942\n", + " \"\"\"\n", + " return (1 + annual_rate) ** (1 / periods) - 1\n", + "data_min = yf.download(\n", + " \"^IRX\",\n", + " start=start_date,\n", + " end=end_date,\n", + " interval=interval,\n", + " progress=False,\n", + " multi_level_index=False,\n", + ")\n", + "\n", + "data_min.columns = data_min.columns.str.lower()\n", + "data_min[\"daily\"] = (data_min[\"close\"]/100).apply(deannualize)\n", + "data_min[\"annualized\"] = data_min[\"close\"] / 100\n", + "data_min[\"name\"] = \"^IRX\"\n", + "data_min[\"description\"] = \"13 WEEK TREASURY BILL\"\n", + "data_min.index.name = \"Datetime\"\n", + "data_min = data_min[[\"name\", \"description\", \"daily\", \"annualized\"]]\n", + "data_min = data_min[\n", + " (data_min.index >= pd.to_datetime(start_date)) & (data_min.index <= pd.to_datetime(end_date))\n", + "]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "afe9009e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedescriptiondailyannualized
Datetime
2022-12-27^IRX13 WEEK TREASURY BILL0.0001130.04195
2022-12-28^IRX13 WEEK TREASURY BILL0.0001160.04338
2022-12-29^IRX13 WEEK TREASURY BILL0.0001150.04295
2022-12-30^IRX13 WEEK TREASURY BILL0.0001140.04260
2023-01-03^IRX13 WEEK TREASURY BILL0.0001140.04255
...............
2023-12-29^IRX13 WEEK TREASURY BILL0.0001380.05180
2024-01-02^IRX13 WEEK TREASURY BILL0.0001390.05213
2024-01-03^IRX13 WEEK TREASURY BILL0.0001400.05235
2024-01-04^IRX13 WEEK TREASURY BILL0.0001400.05228
2024-01-05^IRX13 WEEK TREASURY BILL0.0001390.05220
\n", + "

258 rows × 4 columns

\n", + "
" + ], + "text/plain": [ + " name description daily annualized\n", + "Datetime \n", + "2022-12-27 ^IRX 13 WEEK TREASURY BILL 0.000113 0.04195\n", + "2022-12-28 ^IRX 13 WEEK TREASURY BILL 0.000116 0.04338\n", + "2022-12-29 ^IRX 13 WEEK TREASURY BILL 0.000115 0.04295\n", + "2022-12-30 ^IRX 13 WEEK TREASURY BILL 0.000114 0.04260\n", + "2023-01-03 ^IRX 13 WEEK TREASURY BILL 0.000114 0.04255\n", + "... ... ... ... ...\n", + "2023-12-29 ^IRX 13 WEEK TREASURY BILL 0.000138 0.05180\n", + "2024-01-02 ^IRX 13 WEEK TREASURY BILL 0.000139 0.05213\n", + "2024-01-03 ^IRX 13 WEEK TREASURY BILL 0.000140 0.05235\n", + "2024-01-04 ^IRX 13 WEEK TREASURY BILL 0.000140 0.05228\n", + "2024-01-05 ^IRX 13 WEEK TREASURY BILL 0.000139 0.05220\n", + "\n", + "[258 rows x 4 columns]" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data_min\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/notebooks/test_all.ipynb b/trade/datamanager/notebooks/test_all.ipynb new file mode 100644 index 0000000..2459be0 --- /dev/null +++ b/trade/datamanager/notebooks/test_all.ipynb @@ -0,0 +1,4262 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "765af416", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:01 trade.helpers.Logging INFO: Logging Root Directory: /Users/chiemelienwanisobi/cloned_repos/QuantTools/logs\n", + "2026-02-01 01:08:01 [test] trade.helpers.clear_cache INFO: No expired caches to delete on 2026-02-01.\n", + "2026-02-01 01:08:04 [test] dbase.DataAPI.ThetaData.proxy INFO: Refreshed proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-01 01:08:04 [test] dbase.DataAPI.ThetaData.proxy INFO: Using Proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-01 01:08:04 [test] dbase.DataAPI.ThetaData INFO: Using V2 of the ThetaData API\n", + "\n", + "\n", + "Scheduled Data Requests will be saved to: /Users/chiemelienwanisobi/cloned_repos/QuantTools/module_test/raw_code/DataManagers/scheduler/requests.jsonl\n", + "2026-02-01 01:08:07 [test] DataManager.py CRITICAL: Using ProcessSaveManager for saving data.\n", + "Fetching rates data from yfinance directly during market hours\n", + "YF.download() has changed argument auto_adjust default to True\n" + ] + }, + { + "data": { + "text/plain": [ + "set()" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from trade.datamanager import (\n", + " DividendDataManager,\n", + " SpotDataManager,\n", + " OptionSpotDataManager,\n", + " VolDataManager,\n", + " RatesDataManager,\n", + " BaseDataManager,\n", + " ForwardDataManager,\n", + " GreekDataManager,\n", + " assert_synchronized_model,\n", + " get_option_theoretical_price,\n", + " calculate_scenarios\n", + ")\n", + "\n", + "from trade.datamanager._enums import OptionSpotEndpointSource, SeriesId, OptionPricingModel, VolatilityModel, RealTimeFallbackOption, GreekType, ModelPrice\n", + "from trade.optionlib.config.types import DivType\n", + "from trade.helpers.helper_types import SingletonMetaClass\n", + "from trade.datamanager.vars import get_loaded_names, TS\n", + "get_loaded_names()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "071f3d53", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for DividendDataManager (CACHE_NAME='dividend_data_manager')\n", + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for RatesDataManager (CACHE_NAME='rates_data_manager')\n", + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for ForwardDataManager (CACHE_NAME='forward_data_manager')\n", + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for OptionSpotDataManager (CACHE_NAME='option_spot_manager')\n", + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for SpotDataManager (CACHE_NAME='spot_data_manager')\n", + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for VolDataManager (CACHE_NAME='vol_data_manager_cache')\n", + "2026-02-01 01:08:12 [test] trade.datamanager.base INFO: Clearing cache for GreekDataManager (CACHE_NAME='greek_datamanager_cache')\n" + ] + } + ], + "source": [ + "BaseDataManager.clear_all_caches()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "41c213d5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys([])" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "SingletonMetaClass._instances.keys()" + ] + }, + { + "cell_type": "markdown", + "id": "dcd84d88", + "metadata": {}, + "source": [ + "## TEST 1:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3bf4a022", + "metadata": {}, + "outputs": [], + "source": [ + "## Vars\n", + "div = DivType.CONTINUOUS\n", + "undo_adjust = True\n", + "endpoint_source = OptionSpotEndpointSource.EOD\n", + "series_id = SeriesId.HIST\n", + "market_model = OptionPricingModel.BSM\n", + "vol_model = VolatilityModel.MARKET\n", + "model_price = ModelPrice.ASK\n", + "\n", + "symbol = \"SBUX\"\n", + "expiration = \"2026-09-18\"\n", + "right = \"C\"\n", + "strike = 100.0\n", + "ts_start = \"2025-01-01\"\n", + "ts_end = \"2026-01-28\"" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ef161d94", + "metadata": {}, + "outputs": [], + "source": [ + "BaseDataManager.CONFIG.dividend_type = div\n", + "BaseDataManager.CONFIG.undo_adjust = undo_adjust\n", + "BaseDataManager.CONFIG.option_spot_endpoint_source = endpoint_source\n", + "BaseDataManager.CONFIG.option_model = market_model\n", + "BaseDataManager.CONFIG.volatility_model = vol_model\n", + "BaseDataManager.CONFIG.model_price = model_price\n", + "BaseDataManager.CONFIG.assert_valid()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "3cd6c7d3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "BaseDataManager.CONFIG.dividend_type\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f0326b78", + "metadata": {}, + "outputs": [], + "source": [ + "div_dm = DividendDataManager(symbol=symbol)\n", + "spot_dm = SpotDataManager(symbol=symbol)\n", + "option_spot_dm = OptionSpotDataManager(symbol=symbol)\n", + "vol_dm = VolDataManager(symbol=symbol)\n", + "rates_dm = RatesDataManager()\n", + "fwd_dm = ForwardDataManager(symbol=symbol)\n", + "greek_dm = GreekDataManager(symbol=symbol)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d8513cb3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:14 [test] trade.datamanager.vars INFO: Loading timeseries for SBUX...\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:14 [test] EventDriven.riskmanager.market_data INFO: Timeseries for SBUX already loaded. Use force=False to reload.\n", + "2026-02-01 01:08:14 [test] trade.datamanager.dividend INFO: Using config default dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:08:14 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:15 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:15 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:15 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:15 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:08:15 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:16 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:16 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:16 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:17 [test] trade.datamanager.rates INFO: No cache found for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching from source.\n", + "2026-02-01 01:08:20 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D to avoid saving partial day data.\n", + "2026-02-01 01:08:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:08:20 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:08:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:08:20 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:20 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:08:20 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:08:20 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:20 [test] trade.datamanager.option_spot INFO: No cache found for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100. Fetching from source.\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100 to avoid saving partial day data.\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:22 [test] trade.datamanager.vol INFO: VolDm Using default dividend type from config: DivType.CONTINUOUS\n", + "2026-02-01 01:08:22 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:08:22 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:22 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:22 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:22 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:08:22 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:08:22 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:08:23 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:08:23 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:23 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:23 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:08:23 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:23 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:08:23 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:23 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:23 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:23 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:23 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:23 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:23 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:08:24 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:08:24 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:24 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:24 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-01 01:08:24 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:08:24 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:08:24 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:08:25 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:08:25 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:08:25 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:08:25 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n" + ] + } + ], + "source": [ + "div_data = div_dm.get_schedule_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " maturity_date=expiration,\n", + ")\n", + "\n", + "fwd_data = fwd_dm.get_forward_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " maturity_date=expiration,\n", + ")\n", + "\n", + "spot_data = spot_dm.get_spot_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")\n", + "\n", + "option_spot_data = option_spot_dm.get_option_spot_timeseries(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")\n", + "\n", + "vol_data = vol_dm.get_implied_volatility_timeseries(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")\n", + "\n", + "greek_data = greek_dm.get_greeks_timeseries(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a60de8d7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dividend Type from Config: DivType.CONTINUOUS\n", + "Dividend Type from Dividend DataManager: DivType.CONTINUOUS\n", + "Dividend Type from Dividend Data: DivType.CONTINUOUS\n", + "\n", + "\n", + "Dividend Type from ForwardDataManager: DivType.CONTINUOUS\n", + "Dividend Type from Forward Data: DivType.CONTINUOUS\n", + "\n", + "\n", + "Dividend Type from SpotDataManager: DivType.CONTINUOUS\n", + "\n", + "\n", + "Dividend Type from OptionSpotDataManager: DivType.CONTINUOUS\n", + "\n", + "\n", + "Dividend Type from VolDataManager: DivType.CONTINUOUS\n", + "Dividend Type from Vol Data: DivType.CONTINUOUS\n", + "\n", + "\n", + "Dividend Type from GreekDataManager: DivType.CONTINUOUS\n", + "Dividend Type from Greek Data: DivType.CONTINUOUS\n" + ] + } + ], + "source": [ + "print(f\"Dividend Type from Config: {BaseDataManager.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Dividend DataManager: {div_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Dividend Data: {div_data.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from ForwardDataManager: {fwd_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Forward Data: {fwd_data.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from SpotDataManager: {spot_dm.CONFIG.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from OptionSpotDataManager: {option_spot_dm.CONFIG.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from VolDataManager: {vol_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Vol Data: {vol_data.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from GreekDataManager: {greek_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Greek Data: {greek_data.dividend_type}\")\n", + "# div_data.dividend_type\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "063845bc", + "metadata": {}, + "outputs": [], + "source": [ + "assert_synchronized_model(\n", + " symbol=symbol,\n", + " undo_adjust=undo_adjust,\n", + " dividend_type=div,\n", + " spot = spot_data,\n", + " dividend = div_data,\n", + " forward = fwd_data,\n", + " option_spot = option_spot_data,\n", + " vol = vol_data,\n", + " greek=greek_data,\n", + " model_price=model_price)\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e0c65095", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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v78fpaWl+NKXvoTzzz9fe51bb70Vra2tbq89a9Ys3H333QCAvr4+PPfcc/j444/R1dWFwsJCLFu2DBdffDEsFgtaWlpw2223jVrfFVdcgZUrV2LdunVuawKAlStX4oILLsCsWbOwbt06tLS0oLq6GjfffDMmTZqEt956Cy+//DI6OjowdepU3HLLLSgpKXF7/f3792PdunWoq6uDy+XClClTcM0112DGjBmR+ogJgggS8rAQBOGVY8eO4d5770Vubi6uvPJKuFwurFu3Dna73W2/jRs3YuLEiTjllFOQkpKCTz/9FI8++igURcHy5csBAF//+tfx2GOPIT09HZdddhkAaK8zNDSEu+++Gx0dHTj33HNRVFSEffv24W9/+xscDgduuOEG5Obm4qabbsKjjz6KU089FaeeeioAoKqqyq8Ne/fuxZYtW3DBBRcAAF588UX88pe/xMUXX4yNGzfiggsuQG9vL15++WX88Y9/xM9+9jPt2J07d+K+++5DTU0NrrzySjDG8N577+Gee+7BPffcg9ra2kh8zARBBAkJFoIgvPLss8+Cc4577rkHRUVFAIBFixbhBz/4gdt+P//5z5Gamqo9Xr58OVavXo3XXntNEyynnnoqnn32WeTk5GDx4sVux7/66qtoamrCr371K5SXlwMAzjvvPBQUFODll1/GihUrUFRUhNNOOw2PPvooJk2aNOo1fNHQ0IDf/va3muckOzsbf/7zn/H888/jd7/7HTIyMgAAiqLgxRdfREtLC0pKSsA5xyOPPILZs2fjjjvuAGNMW9ftt9+OZ555Bj/5yU9C/UgJghgDlHRLEMQoFEXBtm3bsHDhQk2sAEBlZSXmz5/vtq9RrPT396O7uxuzZs1Cc3Mz+vv7A77XRx99hJkzZyIrKwvd3d3av7lz50JRFOzZsydsO+bMmeMW5pFekUWLFmliBQCmTp0KAGhpaQEAHDlyBI2NjTjrrLPQ09OjrWlwcBBz5szBnj17oChK2OsiCCJ0yMNCEMQouru7MTw8rHk8jFRUVOCzzz7THu/duxfPPfcc6urqMDQ05LZvf38/MjMz/b5XY2Mjjh49iptuusnr811dXWFYIDCKLQDaWgoLC71u7+3t1dYEAGvXrvX52v39/cjOzg57bQRBhAYJFoIgwqapqQm/+MUvUFFRga997WsoLCyE1WrFZ599htdeey0oLwTnHPPmzcPFF1/s9fmKioqw12exeHci+9puXBMAXH/99aiurva6T3p6etjrIggidEiwEAQxitzcXKSmpmqeBiMNDQ3a/U8//RQjIyP40Y9+5ObNCKW5W2lpKQYHBzFv3jy/+8k8klhQWloKQHheAq2LIIjYQDksBEGMwmKxYP78+fjkk0/Q1tambT9x4gS2bdvmth+geyQAESrx1iAuPT0dfX19o7affvrpqKurw+effz7qub6+PrhcLgBAWlqa9vrRpqamBqWlpXjllVcwODg46vnu7u6or4EgCHfIw0IQhFdWrlyJzz//HD/96U9x/vnnQ1EUvP7665g4cSKOHj0KAJg/fz6sVivWrFmDc889F4ODg3jnnXeQm5uLzs5Ot9ebPHky3nrrLfz9739HWVkZ8vLyMGfOHFx88cXYsmUL1qxZg3POOQc1NTUYGhrCsWPH8NFHH2Ht2rWax6eyshKbN29GeXk5srOzMXHiREyaNCnitlssFnzrW9/Cfffdh9tvvx1LlixBQUEBOjo6sGvXLmRkZODHP/5xxN+XIAjfkGAhCMIrVVVVuPPOO/H4449j3bp1KCwsxMqVK9HZ2akJloqKCtx+++149tln8cQTT8But+P8889Hbm4u/vjHP7q93hVXXIG2tja8/PLLGBgYwKxZszBnzhykpaXh5z//OZ5//nl89NFH+Mc//oGMjAxUVFRg5cqVbkm73/rWt/CXv/wFf/3rX+F0OnHFFVdERbAAwOzZs7F69WqsX78eb775JgYHB2G321FbW4vzzjsvKu9JEIRvqNMtQRAEQRCmh3JYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPSRYCIIgCIIwPf8/sdnv8kevJeAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "greek_data.delta.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ae4c65c6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:26 [test] trade.datamanager.utils WARNING: Valuation date 2026-02-01 01:08:26.642208 is not a business day or holiday. Resolving using fallback options RealTimeFallbackOption.USE_LAST_AVAILABLE.\n", + "2026-02-01 01:08:26 [test] trade.datamanager.utils INFO: Using last available business day for valuation date.\n", + "2026-02-01 01:08:26 [test] trade.datamanager.utils INFO: New valuation date: 2026-01-30 01:08:26\n", + "2026-02-01 01:08:26 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 01:08:26 - 2026-01-30 01:08:26 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:26 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:08:26 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:26 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:08:26 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:27 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:27 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:27 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:27 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-30 01:08:26 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:08:27 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 00:00:00 to 2026-01-30 01:08:26...\n", + "2026-02-01 01:08:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:27 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-01-30 00:00:00')]\n", + "2026-02-01 01:08:27 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:27 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:28 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:28 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:28 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:08:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 00:00:00 to 2026-01-30 01:08:26...\n", + "2026-02-01 01:08:28 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-01 01:08:28 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:08:28 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 01:08:26 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:28 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:08:28 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:28 [test] trade.datamanager.option_spot INFO: No cache found for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100. Fetching from source.\n", + "2026-02-01 01:08:28 [test] trade.datamanager.option_spot INFO: Fetching option spot data from Thetadata Quote endpoint for SBUX from 2026-01-30 00:00:00 to 2026-01-30 00:00:00.\n", + "2026-02-01 01:08:29 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100 to avoid saving partial day data.\n", + "2026-02-01 01:08:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:29 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:continuous|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:08:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:30 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:08:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
gammavolgarhodeltathetavega
datetime
2026-01-300.0177350.0005660.2148520.437738-0.0221270.287246
\n", + "
" + ], + "text/plain": [ + " gamma volga rho delta theta vega\n", + "datetime \n", + "2026-01-30 0.017735 0.000566 0.214852 0.437738 -0.022127 0.287246" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "greek_dm.rt(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + ").timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "9e1cd59b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:30 [test] trade.datamanager.vol WARNING: Valuation date 2026-02-01 00:00:00 is not a business day or holiday. Resolving using fallback options RealTimeFallbackOption.USE_LAST_AVAILABLE.\n", + "2026-02-01 01:08:30 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-01 01:08:30 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:08:30 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:30 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:08:30 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:30 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:30 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:30 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:30 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:08:30 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2026-01-30 to 2026-01-30 with maturity 2026-09-18\n", + "2026-02-01 01:08:30 [test] trade.datamanager.dividend INFO: No cache found for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1. Building from scratch.\n", + "2026-02-01 01:08:30 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:32 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker SBUX\n", + "2026-02-01 01:08:32 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 11, for original valuation: 3. Size from historical divs: 8\n", + "2026-02-01 01:08:32 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 3\n", + "2026-02-01 01:08:32 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.62, 0.62, 0.62]\n", + "2026-02-01 01:08:32 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.57, 0.57, 0.57, 0.61, 0.61, 0.61, 0.61, 0.62, 0.62, 0.62, 0.62]\n", + "2026-02-01 01:08:32 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2024, 2, 8), datetime.date(2024, 5, 16), datetime.date(2024, 8, 16), datetime.date(2024, 11, 15), datetime.date(2025, 2, 14), datetime.date(2025, 5, 16), datetime.date(2025, 8, 15), datetime.date(2025, 11, 14), datetime.date(2026, 2, 14), datetime.date(2026, 5, 14), datetime.date(2026, 8, 14)]\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1 to avoid saving partial day data.\n", + "2026-02-01 01:08:32 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:32 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:32 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:32 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 00:00:00 to 2026-01-30 00:00:00...\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 00:00:00 to 2026-01-30 00:00:00...\n", + "2026-02-01 01:08:32 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:08:32 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-30 0.324693\n", + "dtype: float64" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vol_dm.rt(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + ").timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "545ad9cd", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vol_data.timeseries.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "bed78e85", + "metadata": {}, + "source": [ + "## TEST 2:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "0cbc6e41", + "metadata": {}, + "outputs": [], + "source": [ + "## Vars\n", + "div = DivType.DISCRETE\n", + "undo_adjust = True\n", + "endpoint_source = OptionSpotEndpointSource.QUOTE\n", + "series_id = SeriesId.HIST\n", + "market_model = OptionPricingModel.BINOMIAL\n", + "vol_model = VolatilityModel.MARKET\n", + "\n", + "# symbol = \"AMD\"\n", + "# expiration = \"2027-12-17\"\n", + "# right = \"P\"\n", + "# strike = 210.0\n", + "# ts_start = \"2025-01-01\"\n", + "# ts_end = \"2026-01-23\"" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ab697fe2", + "metadata": {}, + "outputs": [], + "source": [ + "BaseDataManager.CONFIG.dividend_type = div\n", + "BaseDataManager.CONFIG.undo_adjust = undo_adjust\n", + "BaseDataManager.CONFIG.option_spot_endpoint_source = endpoint_source\n", + "BaseDataManager.CONFIG.option_model = market_model\n", + "BaseDataManager.CONFIG.volatility_model = vol_model\n", + "BaseDataManager.CONFIG.assert_valid()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "61aa0800", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "BaseDataManager.CONFIG.dividend_type\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "7a443073", + "metadata": {}, + "outputs": [], + "source": [ + "div_dm = DividendDataManager(symbol=symbol)\n", + "spot_dm = SpotDataManager(symbol=symbol)\n", + "option_spot_dm = OptionSpotDataManager(symbol=symbol)\n", + "vol_dm = VolDataManager(symbol=symbol)\n", + "rates_dm = RatesDataManager()\n", + "fwd_dm = ForwardDataManager(symbol=symbol)\n", + "greek_dm = GreekDataManager(symbol=symbol)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "8ae00d36", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:34 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Using config default dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-01-01 to 2026-01-28 with maturity 2026-09-18\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1. Fetching missing dates: [Timestamp('2025-01-02 00:00:00'), Timestamp('2025-01-03 00:00:00'), Timestamp('2025-01-06 00:00:00'), Timestamp('2025-01-07 00:00:00'), Timestamp('2025-01-08 00:00:00'), Timestamp('2025-01-10 00:00:00'), Timestamp('2025-01-13 00:00:00'), Timestamp('2025-01-14 00:00:00'), Timestamp('2025-01-15 00:00:00'), Timestamp('2025-01-16 00:00:00'), Timestamp('2025-01-17 00:00:00'), Timestamp('2025-01-21 00:00:00'), Timestamp('2025-01-22 00:00:00'), Timestamp('2025-01-23 00:00:00'), Timestamp('2025-01-24 00:00:00'), Timestamp('2025-01-27 00:00:00'), Timestamp('2025-01-28 00:00:00'), Timestamp('2025-01-29 00:00:00'), Timestamp('2025-01-30 00:00:00'), Timestamp('2025-01-31 00:00:00'), Timestamp('2025-02-03 00:00:00'), Timestamp('2025-02-04 00:00:00'), Timestamp('2025-02-05 00:00:00'), Timestamp('2025-02-06 00:00:00'), Timestamp('2025-02-07 00:00:00'), Timestamp('2025-02-10 00:00:00'), Timestamp('2025-02-11 00:00:00'), Timestamp('2025-02-12 00:00:00'), Timestamp('2025-02-13 00:00:00'), Timestamp('2025-02-14 00:00:00'), Timestamp('2025-02-18 00:00:00'), Timestamp('2025-02-19 00:00:00'), Timestamp('2025-02-20 00:00:00'), Timestamp('2025-02-21 00:00:00'), Timestamp('2025-02-24 00:00:00'), Timestamp('2025-02-25 00:00:00'), Timestamp('2025-02-26 00:00:00'), Timestamp('2025-02-27 00:00:00'), Timestamp('2025-02-28 00:00:00'), Timestamp('2025-03-03 00:00:00'), Timestamp('2025-03-04 00:00:00'), Timestamp('2025-03-05 00:00:00'), Timestamp('2025-03-06 00:00:00'), Timestamp('2025-03-07 00:00:00'), Timestamp('2025-03-10 00:00:00'), Timestamp('2025-03-11 00:00:00'), Timestamp('2025-03-12 00:00:00'), Timestamp('2025-03-13 00:00:00'), Timestamp('2025-03-14 00:00:00'), Timestamp('2025-03-17 00:00:00'), Timestamp('2025-03-18 00:00:00'), Timestamp('2025-03-19 00:00:00'), Timestamp('2025-03-20 00:00:00'), Timestamp('2025-03-21 00:00:00'), Timestamp('2025-03-24 00:00:00'), Timestamp('2025-03-25 00:00:00'), Timestamp('2025-03-26 00:00:00'), Timestamp('2025-03-27 00:00:00'), Timestamp('2025-03-28 00:00:00'), Timestamp('2025-03-31 00:00:00'), Timestamp('2025-04-01 00:00:00'), Timestamp('2025-04-02 00:00:00'), Timestamp('2025-04-03 00:00:00'), Timestamp('2025-04-04 00:00:00'), Timestamp('2025-04-07 00:00:00'), Timestamp('2025-04-08 00:00:00'), Timestamp('2025-04-09 00:00:00'), Timestamp('2025-04-10 00:00:00'), Timestamp('2025-04-11 00:00:00'), Timestamp('2025-04-14 00:00:00'), Timestamp('2025-04-15 00:00:00'), Timestamp('2025-04-16 00:00:00'), Timestamp('2025-04-17 00:00:00'), Timestamp('2025-04-21 00:00:00'), Timestamp('2025-04-22 00:00:00'), Timestamp('2025-04-23 00:00:00'), Timestamp('2025-04-24 00:00:00'), Timestamp('2025-04-25 00:00:00'), Timestamp('2025-04-28 00:00:00'), Timestamp('2025-04-29 00:00:00'), Timestamp('2025-04-30 00:00:00'), Timestamp('2025-05-01 00:00:00'), Timestamp('2025-05-02 00:00:00'), Timestamp('2025-05-05 00:00:00'), Timestamp('2025-05-06 00:00:00'), Timestamp('2025-05-07 00:00:00'), Timestamp('2025-05-08 00:00:00'), Timestamp('2025-05-09 00:00:00'), Timestamp('2025-05-12 00:00:00'), Timestamp('2025-05-13 00:00:00'), Timestamp('2025-05-14 00:00:00'), Timestamp('2025-05-15 00:00:00'), Timestamp('2025-05-16 00:00:00'), Timestamp('2025-05-19 00:00:00'), Timestamp('2025-05-20 00:00:00'), Timestamp('2025-05-21 00:00:00'), Timestamp('2025-05-22 00:00:00'), Timestamp('2025-05-23 00:00:00'), Timestamp('2025-05-27 00:00:00'), Timestamp('2025-05-28 00:00:00'), Timestamp('2025-05-29 00:00:00'), Timestamp('2025-05-30 00:00:00'), Timestamp('2025-06-02 00:00:00'), Timestamp('2025-06-03 00:00:00'), Timestamp('2025-06-04 00:00:00'), Timestamp('2025-06-05 00:00:00'), Timestamp('2025-06-06 00:00:00'), Timestamp('2025-06-09 00:00:00'), Timestamp('2025-06-10 00:00:00'), Timestamp('2025-06-11 00:00:00'), Timestamp('2025-06-12 00:00:00'), Timestamp('2025-06-13 00:00:00'), Timestamp('2025-06-16 00:00:00'), Timestamp('2025-06-17 00:00:00'), Timestamp('2025-06-18 00:00:00'), Timestamp('2025-06-20 00:00:00'), Timestamp('2025-06-23 00:00:00'), Timestamp('2025-06-24 00:00:00'), Timestamp('2025-06-25 00:00:00'), Timestamp('2025-06-26 00:00:00'), Timestamp('2025-06-27 00:00:00'), Timestamp('2025-06-30 00:00:00'), Timestamp('2025-07-01 00:00:00'), Timestamp('2025-07-02 00:00:00'), Timestamp('2025-07-03 00:00:00'), Timestamp('2025-07-07 00:00:00'), Timestamp('2025-07-08 00:00:00'), Timestamp('2025-07-09 00:00:00'), Timestamp('2025-07-10 00:00:00'), Timestamp('2025-07-11 00:00:00'), Timestamp('2025-07-14 00:00:00'), Timestamp('2025-07-15 00:00:00'), Timestamp('2025-07-16 00:00:00'), Timestamp('2025-07-17 00:00:00'), Timestamp('2025-07-18 00:00:00'), Timestamp('2025-07-21 00:00:00'), Timestamp('2025-07-22 00:00:00'), Timestamp('2025-07-23 00:00:00'), Timestamp('2025-07-24 00:00:00'), Timestamp('2025-07-25 00:00:00'), Timestamp('2025-07-28 00:00:00'), Timestamp('2025-07-29 00:00:00'), Timestamp('2025-07-30 00:00:00'), Timestamp('2025-07-31 00:00:00'), Timestamp('2025-08-01 00:00:00'), Timestamp('2025-08-04 00:00:00'), Timestamp('2025-08-05 00:00:00'), Timestamp('2025-08-06 00:00:00'), Timestamp('2025-08-07 00:00:00'), Timestamp('2025-08-08 00:00:00'), Timestamp('2025-08-11 00:00:00'), Timestamp('2025-08-12 00:00:00'), Timestamp('2025-08-13 00:00:00'), Timestamp('2025-08-14 00:00:00'), Timestamp('2025-08-15 00:00:00'), Timestamp('2025-08-18 00:00:00'), Timestamp('2025-08-19 00:00:00'), Timestamp('2025-08-20 00:00:00'), Timestamp('2025-08-21 00:00:00'), Timestamp('2025-08-22 00:00:00'), Timestamp('2025-08-25 00:00:00'), Timestamp('2025-08-26 00:00:00'), Timestamp('2025-08-27 00:00:00'), Timestamp('2025-08-28 00:00:00'), Timestamp('2025-08-29 00:00:00'), Timestamp('2025-09-02 00:00:00'), Timestamp('2025-09-03 00:00:00'), Timestamp('2025-09-04 00:00:00'), Timestamp('2025-09-05 00:00:00'), Timestamp('2025-09-08 00:00:00'), Timestamp('2025-09-09 00:00:00'), Timestamp('2025-09-10 00:00:00'), Timestamp('2025-09-11 00:00:00'), Timestamp('2025-09-12 00:00:00'), Timestamp('2025-09-15 00:00:00'), Timestamp('2025-09-16 00:00:00'), Timestamp('2025-09-17 00:00:00'), Timestamp('2025-09-18 00:00:00'), Timestamp('2025-09-19 00:00:00'), Timestamp('2025-09-22 00:00:00'), Timestamp('2025-09-23 00:00:00'), Timestamp('2025-09-24 00:00:00'), Timestamp('2025-09-25 00:00:00'), Timestamp('2025-09-26 00:00:00'), Timestamp('2025-09-29 00:00:00'), Timestamp('2025-09-30 00:00:00'), Timestamp('2025-10-01 00:00:00'), Timestamp('2025-10-02 00:00:00'), Timestamp('2025-10-03 00:00:00'), Timestamp('2025-10-06 00:00:00'), Timestamp('2025-10-07 00:00:00'), Timestamp('2025-10-08 00:00:00'), Timestamp('2025-10-09 00:00:00'), Timestamp('2025-10-10 00:00:00'), Timestamp('2025-10-13 00:00:00'), Timestamp('2025-10-14 00:00:00'), Timestamp('2025-10-15 00:00:00'), Timestamp('2025-10-16 00:00:00'), Timestamp('2025-10-17 00:00:00'), Timestamp('2025-10-20 00:00:00'), Timestamp('2025-10-21 00:00:00'), Timestamp('2025-10-22 00:00:00'), Timestamp('2025-10-23 00:00:00'), Timestamp('2025-10-24 00:00:00'), Timestamp('2025-10-27 00:00:00'), Timestamp('2025-10-28 00:00:00'), Timestamp('2025-10-29 00:00:00'), Timestamp('2025-10-30 00:00:00'), Timestamp('2025-10-31 00:00:00'), Timestamp('2025-11-03 00:00:00'), Timestamp('2025-11-04 00:00:00'), Timestamp('2025-11-05 00:00:00'), Timestamp('2025-11-06 00:00:00'), Timestamp('2025-11-07 00:00:00'), Timestamp('2025-11-10 00:00:00'), Timestamp('2025-11-11 00:00:00'), Timestamp('2025-11-12 00:00:00'), Timestamp('2025-11-13 00:00:00'), Timestamp('2025-11-14 00:00:00'), Timestamp('2025-11-17 00:00:00'), Timestamp('2025-11-18 00:00:00'), Timestamp('2025-11-19 00:00:00'), Timestamp('2025-11-20 00:00:00'), Timestamp('2025-11-21 00:00:00'), Timestamp('2025-11-24 00:00:00'), Timestamp('2025-11-25 00:00:00'), Timestamp('2025-11-26 00:00:00'), Timestamp('2025-11-28 00:00:00'), Timestamp('2025-12-01 00:00:00'), Timestamp('2025-12-02 00:00:00'), Timestamp('2025-12-03 00:00:00'), Timestamp('2025-12-04 00:00:00'), Timestamp('2025-12-05 00:00:00'), Timestamp('2025-12-08 00:00:00'), Timestamp('2025-12-09 00:00:00'), Timestamp('2025-12-10 00:00:00'), Timestamp('2025-12-11 00:00:00'), Timestamp('2025-12-12 00:00:00'), Timestamp('2025-12-15 00:00:00'), Timestamp('2025-12-16 00:00:00'), Timestamp('2025-12-17 00:00:00'), Timestamp('2025-12-18 00:00:00'), Timestamp('2025-12-19 00:00:00'), Timestamp('2025-12-22 00:00:00'), Timestamp('2025-12-23 00:00:00'), Timestamp('2025-12-24 00:00:00'), Timestamp('2025-12-26 00:00:00'), Timestamp('2025-12-29 00:00:00'), Timestamp('2025-12-30 00:00:00'), Timestamp('2025-12-31 00:00:00'), Timestamp('2026-01-02 00:00:00'), Timestamp('2026-01-05 00:00:00'), Timestamp('2026-01-06 00:00:00'), Timestamp('2026-01-07 00:00:00'), Timestamp('2026-01-08 00:00:00'), Timestamp('2026-01-09 00:00:00'), Timestamp('2026-01-12 00:00:00'), Timestamp('2026-01-13 00:00:00'), Timestamp('2026-01-14 00:00:00'), Timestamp('2026-01-15 00:00:00'), Timestamp('2026-01-16 00:00:00'), Timestamp('2026-01-20 00:00:00'), Timestamp('2026-01-21 00:00:00'), Timestamp('2026-01-22 00:00:00'), Timestamp('2026-01-23 00:00:00'), Timestamp('2026-01-26 00:00:00'), Timestamp('2026-01-27 00:00:00'), Timestamp('2026-01-28 00:00:00')]\n", + "2026-02-01 01:08:34 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:34 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker SBUX\n", + "2026-02-01 01:08:34 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 15, for original valuation: 7. Size from historical divs: 12\n", + "2026-02-01 01:08:34 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 3\n", + "2026-02-01 01:08:34 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.62, 0.62, 0.62]\n", + "2026-02-01 01:08:34 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.53, 0.53, 0.53, 0.57, 0.57, 0.57, 0.57, 0.61, 0.61, 0.61, 0.61, 0.62, 0.62, 0.62, 0.62]\n", + "2026-02-01 01:08:34 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2023, 2, 9), datetime.date(2023, 5, 11), datetime.date(2023, 8, 10), datetime.date(2023, 11, 9), datetime.date(2024, 2, 8), datetime.date(2024, 5, 16), datetime.date(2024, 8, 16), datetime.date(2024, 11, 15), datetime.date(2025, 2, 14), datetime.date(2025, 5, 16), datetime.date(2025, 8, 15), datetime.date(2025, 11, 14), datetime.date(2026, 2, 14), datetime.date(2026, 5, 14), datetime.date(2026, 8, 14)]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:08:34 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:08:34 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1 to avoid saving partial day data.\n", + "2026-02-01 01:08:34 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:34 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2025-01-02 00:00:00'), Timestamp('2025-01-03 00:00:00'), Timestamp('2025-01-06 00:00:00'), Timestamp('2025-01-07 00:00:00'), Timestamp('2025-01-08 00:00:00'), Timestamp('2025-01-10 00:00:00'), Timestamp('2025-01-13 00:00:00'), Timestamp('2025-01-14 00:00:00'), Timestamp('2025-01-15 00:00:00'), Timestamp('2025-01-16 00:00:00'), Timestamp('2025-01-17 00:00:00'), Timestamp('2025-01-21 00:00:00'), Timestamp('2025-01-22 00:00:00'), Timestamp('2025-01-23 00:00:00'), Timestamp('2025-01-24 00:00:00'), Timestamp('2025-01-27 00:00:00'), Timestamp('2025-01-28 00:00:00'), Timestamp('2025-01-29 00:00:00'), Timestamp('2025-01-30 00:00:00'), Timestamp('2025-01-31 00:00:00'), Timestamp('2025-02-03 00:00:00'), Timestamp('2025-02-04 00:00:00'), Timestamp('2025-02-05 00:00:00'), Timestamp('2025-02-06 00:00:00'), Timestamp('2025-02-07 00:00:00'), Timestamp('2025-02-10 00:00:00'), Timestamp('2025-02-11 00:00:00'), Timestamp('2025-02-12 00:00:00'), Timestamp('2025-02-13 00:00:00'), Timestamp('2025-02-14 00:00:00'), Timestamp('2025-02-18 00:00:00'), Timestamp('2025-02-19 00:00:00'), Timestamp('2025-02-20 00:00:00'), Timestamp('2025-02-21 00:00:00'), Timestamp('2025-02-24 00:00:00'), Timestamp('2025-02-25 00:00:00'), Timestamp('2025-02-26 00:00:00'), Timestamp('2025-02-27 00:00:00'), Timestamp('2025-02-28 00:00:00'), Timestamp('2025-03-03 00:00:00'), Timestamp('2025-03-04 00:00:00'), Timestamp('2025-03-05 00:00:00'), Timestamp('2025-03-06 00:00:00'), Timestamp('2025-03-07 00:00:00'), Timestamp('2025-03-10 00:00:00'), Timestamp('2025-03-11 00:00:00'), Timestamp('2025-03-12 00:00:00'), Timestamp('2025-03-13 00:00:00'), Timestamp('2025-03-14 00:00:00'), Timestamp('2025-03-17 00:00:00'), Timestamp('2025-03-18 00:00:00'), Timestamp('2025-03-19 00:00:00'), Timestamp('2025-03-20 00:00:00'), Timestamp('2025-03-21 00:00:00'), Timestamp('2025-03-24 00:00:00'), Timestamp('2025-03-25 00:00:00'), Timestamp('2025-03-26 00:00:00'), Timestamp('2025-03-27 00:00:00'), Timestamp('2025-03-28 00:00:00'), Timestamp('2025-03-31 00:00:00'), Timestamp('2025-04-01 00:00:00'), Timestamp('2025-04-02 00:00:00'), Timestamp('2025-04-03 00:00:00'), Timestamp('2025-04-04 00:00:00'), Timestamp('2025-04-07 00:00:00'), Timestamp('2025-04-08 00:00:00'), Timestamp('2025-04-09 00:00:00'), Timestamp('2025-04-10 00:00:00'), Timestamp('2025-04-11 00:00:00'), Timestamp('2025-04-14 00:00:00'), Timestamp('2025-04-15 00:00:00'), Timestamp('2025-04-16 00:00:00'), Timestamp('2025-04-17 00:00:00'), Timestamp('2025-04-21 00:00:00'), Timestamp('2025-04-22 00:00:00'), Timestamp('2025-04-23 00:00:00'), Timestamp('2025-04-24 00:00:00'), Timestamp('2025-04-25 00:00:00'), Timestamp('2025-04-28 00:00:00'), Timestamp('2025-04-29 00:00:00'), Timestamp('2025-04-30 00:00:00'), Timestamp('2025-05-01 00:00:00'), Timestamp('2025-05-02 00:00:00'), Timestamp('2025-05-05 00:00:00'), Timestamp('2025-05-06 00:00:00'), Timestamp('2025-05-07 00:00:00'), Timestamp('2025-05-08 00:00:00'), Timestamp('2025-05-09 00:00:00'), Timestamp('2025-05-12 00:00:00'), Timestamp('2025-05-13 00:00:00'), Timestamp('2025-05-14 00:00:00'), Timestamp('2025-05-15 00:00:00'), Timestamp('2025-05-16 00:00:00'), Timestamp('2025-05-19 00:00:00'), Timestamp('2025-05-20 00:00:00'), Timestamp('2025-05-21 00:00:00'), Timestamp('2025-05-22 00:00:00'), Timestamp('2025-05-23 00:00:00'), Timestamp('2025-05-27 00:00:00'), Timestamp('2025-05-28 00:00:00'), Timestamp('2025-05-29 00:00:00'), Timestamp('2025-05-30 00:00:00'), Timestamp('2025-06-02 00:00:00'), Timestamp('2025-06-03 00:00:00'), Timestamp('2025-06-04 00:00:00'), Timestamp('2025-06-05 00:00:00'), Timestamp('2025-06-06 00:00:00'), Timestamp('2025-06-09 00:00:00'), Timestamp('2025-06-10 00:00:00'), Timestamp('2025-06-11 00:00:00'), Timestamp('2025-06-12 00:00:00'), Timestamp('2025-06-13 00:00:00'), Timestamp('2025-06-16 00:00:00'), Timestamp('2025-06-17 00:00:00'), Timestamp('2025-06-18 00:00:00'), Timestamp('2025-06-20 00:00:00'), Timestamp('2025-06-23 00:00:00'), Timestamp('2025-06-24 00:00:00'), Timestamp('2025-06-25 00:00:00'), Timestamp('2025-06-26 00:00:00'), Timestamp('2025-06-27 00:00:00'), Timestamp('2025-06-30 00:00:00'), Timestamp('2025-07-01 00:00:00'), Timestamp('2025-07-02 00:00:00'), Timestamp('2025-07-03 00:00:00'), Timestamp('2025-07-07 00:00:00'), Timestamp('2025-07-08 00:00:00'), Timestamp('2025-07-09 00:00:00'), Timestamp('2025-07-10 00:00:00'), Timestamp('2025-07-11 00:00:00'), Timestamp('2025-07-14 00:00:00'), Timestamp('2025-07-15 00:00:00'), Timestamp('2025-07-16 00:00:00'), Timestamp('2025-07-17 00:00:00'), Timestamp('2025-07-18 00:00:00'), Timestamp('2025-07-21 00:00:00'), Timestamp('2025-07-22 00:00:00'), Timestamp('2025-07-23 00:00:00'), Timestamp('2025-07-24 00:00:00'), Timestamp('2025-07-25 00:00:00'), Timestamp('2025-07-28 00:00:00'), Timestamp('2025-07-29 00:00:00'), Timestamp('2025-07-30 00:00:00'), Timestamp('2025-07-31 00:00:00'), Timestamp('2025-08-01 00:00:00'), Timestamp('2025-08-04 00:00:00'), Timestamp('2025-08-05 00:00:00'), Timestamp('2025-08-06 00:00:00'), Timestamp('2025-08-07 00:00:00'), Timestamp('2025-08-08 00:00:00'), Timestamp('2025-08-11 00:00:00'), Timestamp('2025-08-12 00:00:00'), Timestamp('2025-08-13 00:00:00'), Timestamp('2025-08-14 00:00:00'), Timestamp('2025-08-15 00:00:00'), Timestamp('2025-08-18 00:00:00'), Timestamp('2025-08-19 00:00:00'), Timestamp('2025-08-20 00:00:00'), Timestamp('2025-08-21 00:00:00'), Timestamp('2025-08-22 00:00:00'), Timestamp('2025-08-25 00:00:00'), Timestamp('2025-08-26 00:00:00'), Timestamp('2025-08-27 00:00:00'), Timestamp('2025-08-28 00:00:00'), Timestamp('2025-08-29 00:00:00'), Timestamp('2025-09-02 00:00:00'), Timestamp('2025-09-03 00:00:00'), Timestamp('2025-09-04 00:00:00'), Timestamp('2025-09-05 00:00:00'), Timestamp('2025-09-08 00:00:00'), Timestamp('2025-09-09 00:00:00'), Timestamp('2025-09-10 00:00:00'), Timestamp('2025-09-11 00:00:00'), Timestamp('2025-09-12 00:00:00'), Timestamp('2025-09-15 00:00:00'), Timestamp('2025-09-16 00:00:00'), Timestamp('2025-09-17 00:00:00'), Timestamp('2025-09-18 00:00:00'), Timestamp('2025-09-19 00:00:00'), Timestamp('2025-09-22 00:00:00'), Timestamp('2025-09-23 00:00:00'), Timestamp('2025-09-24 00:00:00'), Timestamp('2025-09-25 00:00:00'), Timestamp('2025-09-26 00:00:00'), Timestamp('2025-09-29 00:00:00'), Timestamp('2025-09-30 00:00:00'), Timestamp('2025-10-01 00:00:00'), Timestamp('2025-10-02 00:00:00'), Timestamp('2025-10-03 00:00:00'), Timestamp('2025-10-06 00:00:00'), Timestamp('2025-10-07 00:00:00'), Timestamp('2025-10-08 00:00:00'), Timestamp('2025-10-09 00:00:00'), Timestamp('2025-10-10 00:00:00'), Timestamp('2025-10-13 00:00:00'), Timestamp('2025-10-14 00:00:00'), Timestamp('2025-10-15 00:00:00'), Timestamp('2025-10-16 00:00:00'), Timestamp('2025-10-17 00:00:00'), Timestamp('2025-10-20 00:00:00'), Timestamp('2025-10-21 00:00:00'), Timestamp('2025-10-22 00:00:00'), Timestamp('2025-10-23 00:00:00'), Timestamp('2025-10-24 00:00:00'), Timestamp('2025-10-27 00:00:00'), Timestamp('2025-10-28 00:00:00'), Timestamp('2025-10-29 00:00:00'), Timestamp('2025-10-30 00:00:00'), Timestamp('2025-10-31 00:00:00'), Timestamp('2025-11-03 00:00:00'), Timestamp('2025-11-04 00:00:00'), Timestamp('2025-11-05 00:00:00'), Timestamp('2025-11-06 00:00:00'), Timestamp('2025-11-07 00:00:00'), Timestamp('2025-11-10 00:00:00'), Timestamp('2025-11-11 00:00:00'), Timestamp('2025-11-12 00:00:00'), Timestamp('2025-11-13 00:00:00'), Timestamp('2025-11-14 00:00:00'), Timestamp('2025-11-17 00:00:00'), Timestamp('2025-11-18 00:00:00'), Timestamp('2025-11-19 00:00:00'), Timestamp('2025-11-20 00:00:00'), Timestamp('2025-11-21 00:00:00'), Timestamp('2025-11-24 00:00:00'), Timestamp('2025-11-25 00:00:00'), Timestamp('2025-11-26 00:00:00'), Timestamp('2025-11-28 00:00:00'), Timestamp('2025-12-01 00:00:00'), Timestamp('2025-12-02 00:00:00'), Timestamp('2025-12-03 00:00:00'), Timestamp('2025-12-04 00:00:00'), Timestamp('2025-12-05 00:00:00'), Timestamp('2025-12-08 00:00:00'), Timestamp('2025-12-09 00:00:00'), Timestamp('2025-12-10 00:00:00'), Timestamp('2025-12-11 00:00:00'), Timestamp('2025-12-12 00:00:00'), Timestamp('2025-12-15 00:00:00'), Timestamp('2025-12-16 00:00:00'), Timestamp('2025-12-17 00:00:00'), Timestamp('2025-12-18 00:00:00'), Timestamp('2025-12-19 00:00:00'), Timestamp('2025-12-22 00:00:00'), Timestamp('2025-12-23 00:00:00'), Timestamp('2025-12-24 00:00:00'), Timestamp('2025-12-26 00:00:00'), Timestamp('2025-12-29 00:00:00'), Timestamp('2025-12-30 00:00:00'), Timestamp('2025-12-31 00:00:00'), Timestamp('2026-01-02 00:00:00'), Timestamp('2026-01-05 00:00:00'), Timestamp('2026-01-06 00:00:00'), Timestamp('2026-01-07 00:00:00'), Timestamp('2026-01-08 00:00:00'), Timestamp('2026-01-09 00:00:00'), Timestamp('2026-01-12 00:00:00'), Timestamp('2026-01-13 00:00:00'), Timestamp('2026-01-14 00:00:00'), Timestamp('2026-01-15 00:00:00'), Timestamp('2026-01-16 00:00:00'), Timestamp('2026-01-20 00:00:00'), Timestamp('2026-01-21 00:00:00'), Timestamp('2026-01-22 00:00:00'), Timestamp('2026-01-23 00:00:00'), Timestamp('2026-01-26 00:00:00'), Timestamp('2026-01-27 00:00:00'), Timestamp('2026-01-28 00:00:00')]\n", + "2026-02-01 01:08:34 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-01-02 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:08:34 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:08:34 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:08:34 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:34 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:34 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:08:34 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-02 to 2026-01-28...\n", + "2026-02-01 01:08:35 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:08:35 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:08:35 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:08:35 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:08:35 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:08:35 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:08:35 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:08:35 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100. Fetching missing dates: [Timestamp('2025-05-23 00:00:00'), Timestamp('2025-05-27 00:00:00'), Timestamp('2025-05-28 00:00:00'), Timestamp('2025-05-29 00:00:00'), Timestamp('2025-05-30 00:00:00'), Timestamp('2025-06-02 00:00:00'), Timestamp('2025-06-03 00:00:00'), Timestamp('2025-06-04 00:00:00'), Timestamp('2025-06-05 00:00:00'), Timestamp('2025-06-06 00:00:00'), Timestamp('2025-06-09 00:00:00'), Timestamp('2025-06-10 00:00:00'), Timestamp('2025-06-11 00:00:00'), Timestamp('2025-06-12 00:00:00'), Timestamp('2025-06-13 00:00:00'), Timestamp('2025-06-16 00:00:00'), Timestamp('2025-06-17 00:00:00'), Timestamp('2025-06-18 00:00:00'), Timestamp('2025-06-20 00:00:00'), Timestamp('2025-06-23 00:00:00'), Timestamp('2025-06-24 00:00:00'), Timestamp('2025-06-25 00:00:00'), Timestamp('2025-06-26 00:00:00'), Timestamp('2025-06-27 00:00:00'), Timestamp('2025-06-30 00:00:00'), Timestamp('2025-07-01 00:00:00'), Timestamp('2025-07-02 00:00:00'), Timestamp('2025-07-03 00:00:00'), Timestamp('2025-07-07 00:00:00'), Timestamp('2025-07-08 00:00:00'), Timestamp('2025-07-09 00:00:00'), Timestamp('2025-07-10 00:00:00'), Timestamp('2025-07-11 00:00:00'), Timestamp('2025-07-14 00:00:00'), Timestamp('2025-07-15 00:00:00'), Timestamp('2025-07-16 00:00:00'), Timestamp('2025-07-17 00:00:00'), Timestamp('2025-07-18 00:00:00'), Timestamp('2025-07-21 00:00:00'), Timestamp('2025-07-22 00:00:00'), Timestamp('2025-07-23 00:00:00'), Timestamp('2025-07-24 00:00:00'), Timestamp('2025-07-25 00:00:00'), Timestamp('2025-07-28 00:00:00'), Timestamp('2025-07-29 00:00:00'), Timestamp('2025-07-30 00:00:00'), Timestamp('2025-07-31 00:00:00'), Timestamp('2025-08-01 00:00:00'), Timestamp('2025-08-04 00:00:00'), Timestamp('2025-08-05 00:00:00'), Timestamp('2025-08-06 00:00:00'), Timestamp('2025-08-07 00:00:00'), Timestamp('2025-08-08 00:00:00'), Timestamp('2025-08-11 00:00:00'), Timestamp('2025-08-12 00:00:00'), Timestamp('2025-08-13 00:00:00'), Timestamp('2025-08-14 00:00:00'), Timestamp('2025-08-15 00:00:00'), Timestamp('2025-08-18 00:00:00'), Timestamp('2025-08-19 00:00:00'), Timestamp('2025-08-20 00:00:00'), Timestamp('2025-08-21 00:00:00'), Timestamp('2025-08-22 00:00:00'), Timestamp('2025-08-25 00:00:00'), Timestamp('2025-08-26 00:00:00'), Timestamp('2025-08-27 00:00:00'), Timestamp('2025-08-28 00:00:00'), Timestamp('2025-08-29 00:00:00'), Timestamp('2025-09-02 00:00:00'), Timestamp('2025-09-03 00:00:00'), Timestamp('2025-09-04 00:00:00'), Timestamp('2025-09-05 00:00:00'), Timestamp('2025-09-08 00:00:00'), Timestamp('2025-09-09 00:00:00'), Timestamp('2025-09-10 00:00:00'), Timestamp('2025-09-11 00:00:00'), Timestamp('2025-09-12 00:00:00'), Timestamp('2025-09-15 00:00:00'), Timestamp('2025-09-16 00:00:00'), Timestamp('2025-09-17 00:00:00'), Timestamp('2025-09-18 00:00:00'), Timestamp('2025-09-19 00:00:00'), Timestamp('2025-09-22 00:00:00'), Timestamp('2025-09-23 00:00:00'), Timestamp('2025-09-24 00:00:00'), Timestamp('2025-09-25 00:00:00'), Timestamp('2025-09-26 00:00:00'), Timestamp('2025-09-29 00:00:00'), Timestamp('2025-09-30 00:00:00'), Timestamp('2025-10-01 00:00:00'), Timestamp('2025-10-02 00:00:00'), Timestamp('2025-10-03 00:00:00'), Timestamp('2025-10-06 00:00:00'), Timestamp('2025-10-07 00:00:00'), Timestamp('2025-10-08 00:00:00'), Timestamp('2025-10-09 00:00:00'), Timestamp('2025-10-10 00:00:00'), Timestamp('2025-10-13 00:00:00'), Timestamp('2025-10-14 00:00:00'), Timestamp('2025-10-15 00:00:00'), Timestamp('2025-10-16 00:00:00'), Timestamp('2025-10-17 00:00:00'), Timestamp('2025-10-20 00:00:00'), Timestamp('2025-10-21 00:00:00'), Timestamp('2025-10-22 00:00:00'), Timestamp('2025-10-23 00:00:00'), Timestamp('2025-10-24 00:00:00'), Timestamp('2025-10-27 00:00:00'), Timestamp('2025-10-28 00:00:00'), Timestamp('2025-10-29 00:00:00'), Timestamp('2025-10-30 00:00:00'), Timestamp('2025-10-31 00:00:00'), Timestamp('2025-11-03 00:00:00'), Timestamp('2025-11-04 00:00:00'), Timestamp('2025-11-05 00:00:00'), Timestamp('2025-11-06 00:00:00'), Timestamp('2025-11-07 00:00:00'), Timestamp('2025-11-10 00:00:00'), Timestamp('2025-11-11 00:00:00'), Timestamp('2025-11-12 00:00:00'), Timestamp('2025-11-13 00:00:00'), Timestamp('2025-11-14 00:00:00'), Timestamp('2025-11-17 00:00:00'), Timestamp('2025-11-18 00:00:00'), Timestamp('2025-11-19 00:00:00'), Timestamp('2025-11-20 00:00:00'), Timestamp('2025-11-21 00:00:00'), Timestamp('2025-11-24 00:00:00'), Timestamp('2025-11-25 00:00:00'), Timestamp('2025-11-26 00:00:00'), Timestamp('2025-11-28 00:00:00'), Timestamp('2025-12-01 00:00:00'), Timestamp('2025-12-02 00:00:00'), Timestamp('2025-12-03 00:00:00'), Timestamp('2025-12-04 00:00:00'), Timestamp('2025-12-05 00:00:00'), Timestamp('2025-12-08 00:00:00'), Timestamp('2025-12-09 00:00:00'), Timestamp('2025-12-10 00:00:00'), Timestamp('2025-12-11 00:00:00'), Timestamp('2025-12-12 00:00:00'), Timestamp('2025-12-15 00:00:00'), Timestamp('2025-12-16 00:00:00'), Timestamp('2025-12-17 00:00:00'), Timestamp('2025-12-18 00:00:00'), Timestamp('2025-12-19 00:00:00'), Timestamp('2025-12-22 00:00:00'), Timestamp('2025-12-23 00:00:00'), Timestamp('2025-12-24 00:00:00'), Timestamp('2025-12-26 00:00:00'), Timestamp('2025-12-29 00:00:00'), Timestamp('2025-12-30 00:00:00'), Timestamp('2025-12-31 00:00:00'), Timestamp('2026-01-02 00:00:00'), Timestamp('2026-01-05 00:00:00'), Timestamp('2026-01-06 00:00:00'), Timestamp('2026-01-07 00:00:00'), Timestamp('2026-01-08 00:00:00'), Timestamp('2026-01-09 00:00:00'), Timestamp('2026-01-12 00:00:00'), Timestamp('2026-01-13 00:00:00'), Timestamp('2026-01-14 00:00:00'), Timestamp('2026-01-15 00:00:00'), Timestamp('2026-01-16 00:00:00'), Timestamp('2026-01-20 00:00:00'), Timestamp('2026-01-21 00:00:00'), Timestamp('2026-01-22 00:00:00'), Timestamp('2026-01-23 00:00:00'), Timestamp('2026-01-26 00:00:00'), Timestamp('2026-01-27 00:00:00'), Timestamp('2026-01-28 00:00:00')]\n", + "2026-02-01 01:08:35 [test] trade.datamanager.option_spot INFO: Cache partially covers requested date range for option spot timeseries. Key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100. Fetching missing dates.\n", + "2026-02-01 01:08:35 [test] trade.datamanager.option_spot INFO: Fetching option spot data from Thetadata Quote endpoint for SBUX from 2025-05-23 00:00:00 to 2026-01-28 00:00:00.\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100 to avoid saving partial day data.\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:02 [test] trade.datamanager.vol INFO: VolDm Using default dividend type from config: DivType.DISCRETE\n", + "2026-02-01 01:09:02 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:09:02 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:02 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:09:02 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-05-23 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-01 01:09:02 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:09:02 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:09:02 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:02 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:02 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:02 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:09:02 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:09:02 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:02 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:09:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:08 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:09:08 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-05-23 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-01 01:09:08 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:09:08 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:09:08 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:08 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:08 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:09:08 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:08 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-01 01:09:08 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:09:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:08 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:09:09 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:09:09 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:10 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:09:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n" + ] + } + ], + "source": [ + "div_data = div_dm.get_schedule_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " maturity_date=expiration,\n", + ")\n", + "\n", + "fwd_data = fwd_dm.get_forward_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " maturity_date=expiration,\n", + ")\n", + "\n", + "spot_data = spot_dm.get_spot_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")\n", + "\n", + "option_spot_data = option_spot_dm.get_option_spot_timeseries(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")\n", + "\n", + "vol_data = vol_dm.get_implied_volatility_timeseries(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")\n", + "\n", + "greek_data = greek_dm.get_greeks_timeseries(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "73fb62c9", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'greek_data' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mgreek_data\u001b[49m\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__dict__\u001b[39m\u001b[38;5;241m.\u001b[39mkeys()\n", + "\u001b[0;31mNameError\u001b[0m: name 'greek_data' is not defined" + ] + } + ], + "source": [ + "greek_data.__dict__.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "fc1b8bf9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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unUr5s6dC1mW0dPTg+LiYhx99NE4+uijU21GSBJtX0lJCa677rq0Vxkm2q7FixfjK1/5CubOnZtqU2xpamrC9u3bMWfOnCCPuPi9jRkzBieccAL+9re/YerUqVolmvhujFN7lJaWOuJ7EyTSPlmWsXjxYixdujTt358zztYpwO/343e/+x1+8IMfoKGhwfSaUJwiibavrw/Lly/Hxx9/jJdeekn7gnt6epCbm6uVITqBZNglLoROuGtN5vfmBLGSLPsKCwvTKlYitSsQCGhxcHG8TZs2DSeffDLWrVuneYqef/55R1UDJcM+n8+H/Pz8tF70kmFXIBDAKaeckvaLHaCIrQcffBDf//73sWvXLlMOhtE+v9+P5uZmXHHFFTh48CCee+45LR8lEAjA7Xabfm95eXmOECvJsE+SJJx88smO+P7Sf8ZOAS+++CK+/vWv4+DBg1i9ejUuvPBC0+viwvX888/j8ssvx/vvv4/p06fjwgsvxJNPPok//OEP2L59O/72t7+hv7/fMUm12WqXgOzLTPuiseuKK67A+++/HxRyXLJkCfbs2YPbb78dALBs2TLHdBFNln0ejyc1BoQgWXYJb4wTeOKJJ7B//3789Kc/xX/9139hwoQJABSvg9G+r3/963j77bdRXl6Oq666Cv/5z39wzz33YPPmzfjLX/6C5uZmLbHdSWS7fVk/l1BjYyN+8IMfYO7cufjv//5vAEqb9vz8fOTn58PtdmNwcBC//e1vsX37dlx66aVYsGCBdtfwwgsv4K233kJvby8YY7juuusckUiVrXYJyL7MtC9auy677DKceuqpml2yLOONN97A7373O0yYMAHXXnut1uXWCWSrfdlql4Bzjq6uLtxxxx248MILMXfuXHz22Wc4dOgQjjrqKFRWViInJwe/+93v8N577+FrX/saTjnlFO0i/9577+Gll15Cb28vAoEArr76akcl6We7fYKsFyw+nw9PP/00/vnPf+InP/kJnnzySezduxecc1RXV+Oss87CzJkzsWvXLtTW1mrdGo35DbIso62tDZWVlek0xUS22iUg+zLTvljtEgwODuKVV16B1+vF6aefniYrQpOt9mWrXYDepXv37t244447cN999+HRRx/F5s2bMWrUKHR0dGD69On4zne+g8bGRpSUlNj+3gA4ch6kbLfPSNYJlrfeegv5+fk46qijtFmGW1tbcfvtt6O5uRkLFy7ESSedhJ6eHmzcuBE9PT1YsWIFJk2a5KgkTCvZapeA7MtM+7LVLkG22petdgns7Dt48CDuu+8+TJw4Ee3t7fja176GnJwc7Nu3D7/4xS9w+eWXY+nSpWSfg3FGUDgBvP7663jkkUdQUVGBlpYW1NTUYNmyZTjhhBNQWlqKr33ta9i3bx++/OUva+qyuroajz32GF577TVMmjTJkV9ittolIPsy075stUuQrfZlq12CcPZ5PB6MGjUKmzZtwqmnnora2loAwOjRo3Huuefi6aefxtKlS8k+B5PxgiUQCOAf//gHXn75ZVxyySVYsGABPvvsM7z88sv417/+hWOPPRZerxczZszAzJkzTbNPirsFMcGfk8hWuwRkX2bal612CbLVvmy1SxCJfZWVlZg1axbef/99zRbhbairq0NOTg6am5tRXV2dZmuCyXb7IiVzpZbK4OAgurq68MUvfhELFy6E2+3G0Ucfjbq6OvT19WmlXHl5eaYfIQB0d3dr86w4jWy1S0D2ZaZ92WqXIFvty1a7BMPZJ0riFy1ahOOPPx5btmzBnj17NG/Dvn37MHbsWMdezLPdvkjJSA9LU1MTqqurwRhDfn4+TjzxRIwdO9Y0WVN5eTkGBwdtSyGHhobQ29uLxx9/HAAc0xwtW+0SkH2ZaV+22iXIVvuy1S5BNPaJCTMLCgpw9tlnY926dbj11ltx6qmnor+/Hx988AGuuuoqAHoSa7rJdvtiIaMEy6ZNm/Doo4/C4/EgPz8fp59+Ok477TStYY8xmWjLli2or6+H2+02Ld+0aRM++ugjvPXWWxg7dixuvPHGtN85ZKtdArIvM+3LVrsE2WpfttoliNU+v98Pt9uNKVOm4KabbsL69evR3t6OQCCA2267Tcv5SPfFPNvti4eMESzbtm3Do48+irPPPhtVVVXYtm0bHnzwQciyjAULFsDr9Wpton0+Hz7//HOcddZZAMwdTevq6tDU1IRvf/vbjpidN1vtEpB9mWlfttolyFb7stUuQTz2Gb1ILpcLF1xwgeO8DdluX7w4XrCID3znzp0oKirC4sWL4Xa7MWfOHAwNDeGVV15BcXEx5s2bp30xPT096Ovr0xrfNDU14R//+AeuuuoqjB07FmPHjk2nSQCy1y4B2ZeZ9mWrXYJstS9b7RIkyr6XXnoJV155pbZdp1zMs92+ROH4pFvxgR84cABVVVWa6wsALr74Yng8Hrz77rumORM+/PBDlJeXo7S0FA8//DBuvPFGtLW1we/3O2bW4Wy1S0D2ZaZ92WqXIFvty1a7BImyr7W1lezLYBznYdm2bRs2b96MqqoqHH300Vq78ZkzZ+KRRx6BLMval1lYWIgFCxZgw4YNOHjwIEpKSsA5x3vvvYf9+/fjhhtuQElJCW6//fa0z1qbrXYJyL7MtC9b7RJkq33ZapeA7Mts+5KFYzwsR44cwc9//nPcd999WnfF22+/Hbt27QIATJ8+HXl5eXjyySdN7zv99NPR39+PvXv3AlAy24eGhpCbm4trrrkGv/zlL9P6JWarXQKyLzPty1a7BNlqX7baJSD7Mtu+ZOMID8vg4CAee+wx5ObmYtWqVdrcKCtXrsRLL72ESZMmobS0FGeccQaeeuopLF68GOXl5Vrcr7a2Fp9//jkAICcnB8uXL9dmqUwn2WqXgOzLTPuy1S5BttqXrXYJyL7Mti8VOMLDkpOTA4/Hg4ULF6KyshKBQAAAcOyxx+LgwYPgnCMvLw+nnHIKxo8fj3vuuQetra1gjKGtrQ2dnZ2YN2+etj2nfInZapeA7MtM+7LVLkG22petdgnIvsy2LxU4ZvJDUUMO6HXmv/71r5GTk4PrrrtOW6+9vR233norAoEAJk6ciE8++QRjxozBt7/9bUfOMpmtdgnIPoVMsy9b7RJkq33ZapeA7FPIVPuSjWMEix0333wzFi9ejIULF2qtoyVJQnNzM3bv3o1PP/0U48aNw8KFC9M70CjJVrsEZF9m2petdgmy1b5stUtA9mW2fYnEETksdhw6dAjNzc1aLwBJkuD3+yFJEqqrq1FdXY358+eneZTRk612Cci+zLQvW+0SZKt92WqXgOzLbPsSjSNyWIwIh8+OHTuQm5urxemefPJJPPzww+js7Ezn8GImW+0SkH2ZaV+22iXIVvuy1S4B2ZfZ9iULx3lYRAOdXbt24YQTTsC2bdvw+9//HkNDQ/jmN7+JUaNGpXmEsZGtdgnIvsy0L1vtEmSrfdlql4Dsy2z7koXjBAug1Jh/8MEHOHToEF544QVceOGFOOecc9I9rLjJVrsEZF9mkq12CbLVvmy1S0D2EVYcKVi8Xi8qKiowe/ZsXHHFFdrU2ZlOttolIPsyk2y1S5Ct9mWrXQKyj7Di2Coh4xTa2US22iUg+zKTbLVLkK32ZatdArKPMOJYwUIQBEEQBCEgaUcQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQRFSsXbsWy5cvT/cwTDhxTARBJBYSLARBpIR//OMfePXVV2N+/+DgINauXYuPPvoocYMiCCJjIMFCEERKeOmll+IWLOvWrbMVLOeffz7+8pe/xDE6giCcjiMnPyQIgogGl8sFl8uV7mEQBJFEaC4hgiBCsmPHDvz5z3/G/v37UVZWhrPPPhtHjhzBunXrsHbtWgDAxo0b8frrr+Pzzz9HX18fqqqq8OUvfxlnnHGGtp0bbrgBra2tpm1Pnz4dt956KwCgt7cXTz75JN5++210dnZi9OjRWLx4Mc4++2xIkoSWlhZ885vfDBrfBRdcgOXLl2Pt2rWmMQHA8uXLceaZZ2L69OlYu3YtWlpaUF9fj+uuuw5jx47Fyy+/jL///e9ob2/H5MmT8Y1vfAOVlZWm7X/66adYu3Ytdu7ciUAggIkTJ+KSSy7B1KlTE/UREwQRIeRhIQjClv379+P2229HcXExLrzwQgQCAaxduxYlJSWm9V566SUcddRRmDt3LlwuF9577z089NBDkGUZS5YsAQBceeWVePjhh5Gbm4tzzz0XALTtDA4O4tZbb0V7eztOP/10lJeX45NPPsFf//pXdHR04KqrrkJxcTGuvfZaPPTQQ5g3bx7mzZsHABg3blxYG3bs2IHNmzfjzDPPBAA8/fTT+PnPf46zzz4bL730Es4880z09PTg73//O37729/illtu0d7b0NCAO+64AxMmTMCFF14IxhheffVV3HbbbbjtttswadKkRHzMBEFECAkWgiBseeKJJ8A5x2233Yby8nIAwAknnIDvf//7pvV++tOfwuv1as+XLFmCVatW4bnnntMEy7x58/DEE0+gqKgICxYsML3/2WefRXNzM+68807U1NQAAL70pS+hrKwMf//737Fs2TKUl5fjxBNPxEMPPYSxY8cGbSMUjY2NuOeeezTPSWFhIf7whz/gqaeewq9+9Svk5eUBAGRZxtNPP42WlhZUVlaCc44HH3wQM2bMwMqVK8EY08Z144034vHHH8f//u//RvuREgQRB5R0SxBEELIs44MPPsDxxx+viRUAqKurwzHHHGNa1yhW+vr60NXVhenTp+PQoUPo6+sbdl9vvfUWpk2bhoKCAnR1dWl/s2bNgizL2L59e8x2zJw50xTmEV6RE044QRMrADB58mQAQEtLCwBg7969aGpqwimnnILu7m5tTAMDA5g5cya2b98OWZZjHhdBENFDHhaCIILo6urC0NCQ5vEwUltbi61bt2rPd+zYgSeffBI7d+7E4OCgad2+vj7k5+eH3VdTUxP27duHa6+91vb1zs7OGCxQMIotANpYRo8ebbu8p6dHGxMAPPDAAyG33dfXh8LCwpjHRhBEdJBgIQgiZpqbm/Gzn/0MtbW1uOKKKzB69Gi43W5s3boVzz33XEReCM45Zs+ejbPPPtv29dra2pjHJ0n2TuRQy41jAoDLL78c9fX1tuvk5ubGPC6CIKKHBAtBEEEUFxfD6/VqngYjjY2N2uP33nsPPp8PN910k8mbEU1zt6qqKgwMDGD27Nlh1xN5JKmgqqoKgOJ5GW5cBEGkBsphIQgiCEmScMwxx+Ddd99FW1ubtvzAgQP44IMPTOsBukcCUEIldg3icnNz0dvbG7T8pJNOws6dO/H+++8Hvdbb24tAIAAAyMnJ0bafbCZMmICqqips2LABAwMDQa93dXUlfQwEQZghDwtBELYsX74c77//Pn7yk5/gjDPOgCzLeOGFF3DUUUdh3759AIBjjjkGbrcbq1evxumnn46BgQG88sorKC4uxpEjR0zbGz9+PF5++WX87W9/Q3V1NUaNGoWZM2fi7LPPxubNm7F69Wp88YtfxIQJEzA4OIj9+/fjrbfewgMPPKB5fOrq6rBp0ybU1NSgsLAQRx11FMaOHZtw2yVJwvXXX4877rgDN954IxYuXIiysjK0t7fjo48+Ql5eHn70ox8lfL8EQYSGBAtBELaMGzcOP/7xj7FmzRqsXbsWo0ePxvLly3HkyBFNsNTW1uLGG2/EE088gUceeQQlJSU444wzUFxcjN/+9rem7V1wwQVoa2vD3//+d/T392P69OmYOXMmcnJy8NOf/hRPPfUU3nrrLbz++uvIy8tDbW0tli9fbkravf766/GnP/0Jf/7zn+H3+3HBBRckRbAAwIwZM7Bq1SqsW7cO//jHPzAwMICSkhJMmjQJX/rSl5KyT4IgQkOdbgmCIAiCcDyUw0IQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOMhwUIQBEEQhOP5/wGorSiqWF9c4QAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "vol_data.timeseries.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "b89b0989", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dividend Type from Config: DivType.DISCRETE\n", + "Dividend Type from Dividend DataManager: DivType.DISCRETE\n", + "Dividend Type from Dividend Data: DivType.DISCRETE\n", + "\n", + "\n", + "Dividend Type from ForwardDataManager: DivType.DISCRETE\n", + "Dividend Type from Forward Data: DivType.DISCRETE\n", + "\n", + "\n", + "Dividend Type from SpotDataManager: DivType.DISCRETE\n", + "\n", + "\n", + "Dividend Type from OptionSpotDataManager: DivType.DISCRETE\n", + "\n", + "\n", + "Dividend Type from VolDataManager: DivType.DISCRETE\n", + "Dividend Type from Vol Data: DivType.DISCRETE\n" + ] + } + ], + "source": [ + "print(f\"Dividend Type from Config: {BaseDataManager.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Dividend DataManager: {div_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Dividend Data: {div_data.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from ForwardDataManager: {fwd_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Forward Data: {fwd_data.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from SpotDataManager: {spot_dm.CONFIG.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from OptionSpotDataManager: {option_spot_dm.CONFIG.dividend_type}\")\n", + "print(\"\\n\")\n", + "print(f\"Dividend Type from VolDataManager: {vol_dm.CONFIG.dividend_type}\")\n", + "print(f\"Dividend Type from Vol Data: {vol_data.dividend_type}\")\n", + "# div_data.dividend_type\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "501eeeff", + "metadata": {}, + "outputs": [], + "source": [ + "assert_synchronized_model(\n", + " symbol=symbol,\n", + " undo_adjust=undo_adjust,\n", + " dividend_type=div,\n", + " spot = spot_data,\n", + " dividend = div_data,\n", + " forward = fwd_data,\n", + " option_spot = option_spot_data,\n", + " vol = vol_data,\n", + " greek= greek_data,\n", + " market_model=market_model,\n", + " vol_model=vol_model,\n", + " require_anchor=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "a6d7a6c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2025-05-23 0.336226\n", + "2025-05-27 0.334374\n", + "2025-05-28 0.325076\n", + "2025-05-29 0.348844\n", + "2025-05-30 0.351742\n", + " ... \n", + "2026-01-22 0.369853\n", + "2026-01-23 0.335153\n", + "2026-01-26 0.340872\n", + "2026-01-27 0.336315\n", + "2026-01-28 0.352491\n", + "Length: 171, dtype: float64" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vol_data.timeseries#.plot()" + ] + }, + { + "cell_type": "markdown", + "id": "6ec3185f", + "metadata": { + "vscode": { + "languageId": "bat" + } + }, + "source": [ + "## TEST 3: Load all" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "69a7b749", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for DividendDataManager (CACHE_NAME='dividend_data_manager')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for RatesDataManager (CACHE_NAME='rates_data_manager')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for ForwardDataManager (CACHE_NAME='forward_data_manager')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for OptionSpotDataManager (CACHE_NAME='option_spot_manager')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for SpotDataManager (CACHE_NAME='spot_data_manager')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for VolDataManager (CACHE_NAME='vol_data_manager_cache')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.base INFO: Clearing cache for GreekDataManager (CACHE_NAME='greek_datamanager_cache')\n", + "2026-02-01 01:09:12 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:12 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:09:12 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:09:12 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:13 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:13 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:09:13 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:13 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:13 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:13 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:09:13 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:14 [test] trade.datamanager.rates INFO: No cache found for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching from source.\n", + "2026-02-01 01:09:17 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D to avoid saving partial day data.\n", + "2026-02-01 01:09:17 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-01 01:09:17 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:09:17 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:17 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:17 [test] trade.datamanager.option_spot INFO: No cache found for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100. Fetching from source.\n", + "2026-02-01 01:09:22 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100 to avoid saving partial day data.\n", + "2026-02-01 01:09:22 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:22 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-01 01:09:22 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:09:22 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:22 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:09:22 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:22 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:22 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:09:23 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:23 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:09:23 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-01 01:09:23 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:23 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:09:23 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:23 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:09:23 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:09:24 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:24 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:09:24 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:09:24 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:09:25 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:09:25 [test] trade.datamanager.utils INFO: Using cached date range for 2025-05-23 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:09:25 [test] trade.datamanager.option_spot INFO: No cache found for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100. Fetching from source.\n", + "2026-02-01 01:09:25 [test] trade.datamanager.option_spot INFO: Fetching option spot data from Thetadata Quote endpoint for SBUX from 2025-05-23 00:00:00 to 2026-01-28 00:00:00.\n", + "2026-02-01 01:10:03 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100 to avoid saving partial day data.\n", + "2026-02-01 01:10:03 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:10:04 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:10:04 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-02-01 01:10:05 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:10:05 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n" + ] + } + ], + "source": [ + "from trade.datamanager.utils.model import LoadRequest, _load_model_data_timeseries\n", + "from trade.helpers.decorators import cProfiler\n", + "from trade.helpers.helper import print_top_cprofile_stats, print_cprofile_internal_time_share\n", + "\n", + "request = LoadRequest(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " series_id=SeriesId.HIST,\n", + " dividend_type=DivType.CONTINUOUS,\n", + " endpoint_source=OptionSpotEndpointSource.EOD,\n", + " vol_model=VolatilityModel.MARKET,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " model_price=ModelPrice.ASK,\n", + " load_spot=True,\n", + " load_dividend=True,\n", + " load_forward=True,\n", + " load_option_spot=True,\n", + " load_vol=True,\n", + " load_greek=True,\n", + " undo_adjust=True,\n", + ")\n", + "\n", + "f = cProfiler(_load_model_data_timeseries)\n", + "res, stats = f(request)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "b7e56aa1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Function CumTime RatioToTop\n", + "------------------------------------------------------------------------------------------------------------------------------------------------------\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/utils/model.py:315 _load_model_data_timeseries 53.0695 1.00\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/option_spot.py:213 get_option_spot_timeseries 43.3073 0.82\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/option_spot.py:378 _query_thetadata_api 43.1941 0.81\n", + "/Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData/v2.py:414 request_from_proxy 42.6873 0.80\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/api.py:14 request 42.6871 0.80\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/sessions.py:500 request 42.6857 0.80\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/sessions.py:673 send 42.6663 0.80\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/socket.py:704 readinto 42.5731 0.80\n", + "~:0 42.5697 0.80\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/greeks.py:158 get_greeks_timeseries 41.9619 0.79\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/greeks.py:314 _get_binomial_greeks 41.9428 0.79\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/vol.py:633 get_implied_volatility_timeseries 41.3872 0.78\n", + "/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/vol.py:307 _get_crr_implied_volatility_timeseries 41.3167 0.78\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/adapters.py:613 send 40.3505 0.76\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/urllib3/connectionpool.py:535 urlope... 40.3484 0.76\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/urllib3/connectionpool.py:379 _make_... 40.3472 0.76\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/http/client.py:1351 getresponse 40.2820 0.76\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/http/client.py:318 begin 40.2818 0.76\n", + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/http/client.py:285 _read_status 40.2809 0.76\n", + "~:0 40.2809 0.76\n" + ] + } + ], + "source": [ + "print_top_cprofile_stats(stats, top_n=20, full_name=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "8ed3dcce", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'date_range_packet': 0.0004010200500488281,\n", + " 'dividend_load_time': 0.6139051914215088,\n", + " 'spot_load_time': 0.31241703033447266,\n", + " 'forward_load_time': 4.112335920333862,\n", + " 'option_spot_load_time': 5.0142059326171875,\n", + " 'vol_load_time': 0.9962790012359619,\n", + " 'greek_load_time': 41.98507881164551}" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res.time_to_load" + ] + }, + { + "cell_type": "markdown", + "id": "cbbc8fa8", + "metadata": {}, + "source": [ + "## Scenarios Calc" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "46e91b06", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:16:29 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:16:29 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:16:29 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2026-01-28 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-01 01:16:29 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:16:29 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:16:29 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:16:29 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 to 2026-01-28...\n", + "2026-02-01 01:16:29 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:16:29 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:16:29 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:16:29 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:16:29 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-01 01:16:29 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-01 01:16:29 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n" + ] + } + ], + "source": [ + "bsm = calculate_scenarios(\n", + " symbol=symbol,\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " as_of=ts_end,\n", + " return_pnl_in_pct=True,\n", + " return_pnl=True\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "33d3463f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0.900.951.001.051.10
-0.02-0.508140-0.308790-0.0677240.2138100.532924
-0.01-0.480935-0.277803-0.0338340.2494030.568825
0.00-0.453823-0.2468920.0000040.2849720.604732
0.01-0.426531-0.2160530.0337900.3205160.640641
0.02-0.396147-0.1852820.0675290.3560350.676549
\n", + "
" + ], + "text/plain": [ + " 0.90 0.95 1.00 1.05 1.10\n", + "-0.02 -0.508140 -0.308790 -0.067724 0.213810 0.532924\n", + "-0.01 -0.480935 -0.277803 -0.033834 0.249403 0.568825\n", + " 0.00 -0.453823 -0.246892 0.000004 0.284972 0.604732\n", + " 0.01 -0.426531 -0.216053 0.033790 0.320516 0.640641\n", + " 0.02 -0.396147 -0.185282 0.067529 0.356035 0.676549" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bsm.grid" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "03a8cab7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.dividend WARNING: Valuation date 2026-02-01 01:10:06.622074 is not a business day or holiday. No dividends available. Resolution: RealTimeFallbackOption.USE_LAST_AVAILABLE\n", + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker SBUX\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 11, for original valuation: 3. Size from historical divs: 8\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 3\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.62, 0.62, 0.62]\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.57, 0.57, 0.57, 0.61, 0.61, 0.61, 0.61, 0.62, 0.62, 0.62, 0.62]\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2024, 2, 8), datetime.date(2024, 5, 16), datetime.date(2024, 8, 16), datetime.date(2024, 11, 15), datetime.date(2025, 2, 14), datetime.date(2025, 5, 16), datetime.date(2025, 8, 15), datetime.date(2025, 11, 14), datetime.date(2026, 2, 14), datetime.date(2026, 5, 14), datetime.date(2026, 8, 14)]\n" + ] + }, + { + "data": { + "text/plain": [ + "2026-01-30 ((2026-02-14, 0.62), (2026-05-14, 0.62), (2026...\n", + "dtype: object" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "BaseDataManager.CONFIG.real_time_fallback_option = RealTimeFallbackOption.USE_LAST_AVAILABLE\n", + "div_dm.rt(\n", + " maturity_date=expiration,\n", + " undo_adjust=True,\n", + " fallback_option=None,\n", + " dividend_type=DivType.DISCRETE\n", + " ).daily_discrete_dividends" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "2b5aebd7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.forward WARNING: Valuation date 2026-02-01 01:10:06.795244 is not a business day or holiday. No dividends available. Resolution: RealTimeFallbackOption.USE_LAST_AVAILABLE\n", + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:10:06 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2026-01-30 to 2026-01-30 with maturity 2026-09-18\n", + "2026-02-01 01:10:06 [test] trade.datamanager.dividend INFO: No cache found for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1. Building from scratch.\n", + "2026-02-01 01:10:06 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker SBUX\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 11, for original valuation: 3. Size from historical divs: 8\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 3\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.62, 0.62, 0.62]\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.57, 0.57, 0.57, 0.61, 0.61, 0.61, 0.61, 0.62, 0.62, 0.62, 0.62]\n", + "2026-02-01 01:10:06 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2024, 2, 8), datetime.date(2024, 5, 16), datetime.date(2024, 8, 16), datetime.date(2024, 11, 15), datetime.date(2025, 2, 14), datetime.date(2025, 5, 16), datetime.date(2025, 8, 15), datetime.date(2025, 11, 14), datetime.date(2026, 2, 14), datetime.date(2026, 5, 14), datetime.date(2026, 8, 14)]\n", + "2026-02-01 01:10:06 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:06 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1 to avoid saving partial day data.\n", + "2026-02-01 01:10:06 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:07 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:07 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:07 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:07 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:07 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:10:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-30 92.169198\n", + "dtype: float64" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fwd_dm.rt(\n", + " maturity_date=expiration,\n", + " dividend_type=DivType.DISCRETE,\n", + " dividend_result=None,\n", + " ).timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "9864175d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:10:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:08 [test] trade.datamanager.forward WARNING: Valuation date 2026-02-01 01:10:08.153362 is not a business day or holiday. No dividends available. Resolution: RealTimeFallbackOption.USE_LAST_AVAILABLE\n", + "2026-02-01 01:10:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:08 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-01 01:10:08 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:08 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:08 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:08:10.925009 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:09 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:09 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:09 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:09 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:09 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-12-21|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-01 01:10:09 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-30 94.353327\n", + "dtype: float64" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "fwd_dm.rt(\n", + " maturity_date=\"2026-12-21\",\n", + " dividend_type=DivType.CONTINUOUS,\n", + " dividend_result=None,\n", + " ).timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "7be51033", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
bid_sizebid_exchangebidbid_conditionask_sizeask_exchangeaskask_conditionmidpointweighted_midpoint
datetime
2026-01-3068495.55054046.3505.95.852941
\n", + "
" + ], + "text/plain": [ + " bid_size bid_exchange bid bid_condition ask_size \\\n", + "datetime \n", + "2026-01-30 684 9 5.5 50 540 \n", + "\n", + " ask_exchange ask ask_condition midpoint weighted_midpoint \n", + "datetime \n", + "2026-01-30 4 6.3 50 5.9 5.852941 " + ] + }, + "execution_count": 32, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "option_spot_dm.rt(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + ").timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "bda2c0fe", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:10:09 [test] trade.datamanager.vol WARNING: Valuation date 2026-02-01 00:00:00 is not a business day or holiday. Resolving using fallback options RealTimeFallbackOption.USE_LAST_AVAILABLE.\n", + "2026-02-01 01:10:09 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-01 01:10:09 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:10:09 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:10:09 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:10:09 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:09 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 01:10:09 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2026-01-30 00:00:00 to 2026-01-30 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-01 01:10:09 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:10:09 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 01:10:09 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:09 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:09 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:09 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:09 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-30 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 01:10:09 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:10 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 00:00:00 to 2026-01-30 00:00:00...\n", + "2026-02-01 01:10:10 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:10:10 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100. Fetching missing dates: [Timestamp('2026-01-30 00:00:00')]\n", + "2026-02-01 01:10:10 [test] trade.datamanager.option_spot INFO: Cache partially covers requested date range for option spot timeseries. Key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100. Fetching missing dates.\n", + "2026-02-01 01:10:10 [test] trade.datamanager.option_spot INFO: Fetching option spot data from Thetadata Quote endpoint for SBUX from 2026-01-30 00:00:00 to 2026-01-30 00:00:00.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100 to avoid saving partial day data.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-30 0.318936\n", + "dtype: float64" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vol_dm.rt(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " dividend_type=DivType.DISCRETE,\n", + ").timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "999fffc3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:10:11 [test] trade.datamanager.vol WARNING: Valuation date 2026-02-01 00:00:00 is not a business day or holiday. Resolving using fallback options RealTimeFallbackOption.USE_LAST_AVAILABLE.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-01 01:10:11 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:11 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:11 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-30 00:00:00 - 2026-01-30 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 00:00:00 to 2026-01-30 00:00:00...\n", + "2026-02-01 01:10:11 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:2026-09-18|model_price:ask|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-01 01:10:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-01-30 0.324693\n", + "dtype: float64" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "vol_dm.rt(\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " market_model=OptionPricingModel.BSM,\n", + " dividend_type=DivType.DISCRETE,\n", + ").timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "55c9bf74", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 01:10:12 [test] algo.__init__ CRITICAL: ALGO_DIR not on main branch; skipping runtime safeguards.\n" + ] + }, + { + "data": { + "text/html": [ + " \n", + "
\n", + " \n", + " Loading BokehJS ...\n", + "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "'use strict';\n(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\nconst JS_MIME_TYPE = 'application/javascript';\n const HTML_MIME_TYPE = 'text/html';\n const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n const CLASS_NAME = 'output_bokeh rendered_html';\n\n /**\n * Render data to the DOM node\n */\n function render(props, node) {\n const script = document.createElement(\"script\");\n node.appendChild(script);\n }\n\n /**\n * Handle when an output is cleared or removed\n */\n function handleClearOutput(event, handle) {\n function drop(id) {\n const view = Bokeh.index.get_by_id(id)\n if (view != null) {\n view.model.document.clear()\n Bokeh.index.delete(view)\n }\n }\n\n const cell = handle.cell;\n\n const id = cell.output_area._bokeh_element_id;\n const server_id = cell.output_area._bokeh_server_id;\n\n // Clean up Bokeh references\n if (id != null) {\n drop(id)\n }\n\n if (server_id !== undefined) {\n // Clean up Bokeh references\n const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n cell.notebook.kernel.execute(cmd_clean, {\n iopub: {\n output: function(msg) {\n const id = msg.content.text.trim()\n drop(id)\n }\n }\n });\n // Destroy server and session\n const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n cell.notebook.kernel.execute(cmd_destroy);\n }\n }\n\n /**\n * Handle when a new output is added\n */\n function handleAddOutput(event, handle) {\n const output_area = handle.output_area;\n const output = handle.output;\n\n // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n return\n }\n\n const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n // store reference to embed id on output_area\n output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n }\n if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n const bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n const script_attrs = bk_div.children[0].attributes;\n for (let i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n }\n\n function register_renderer(events, OutputArea) {\n\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n const toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[toinsert.length - 1]);\n element.append(toinsert);\n return toinsert\n }\n\n /* Handle when an output is cleared or removed */\n events.on('clear_output.CodeCell', handleClearOutput);\n events.on('delete.Cell', handleClearOutput);\n\n /* Handle when a new output is added */\n events.on('output_added.OutputArea', handleAddOutput);\n\n /**\n * Register the mime type and append_mime function with output_area\n */\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n /* Is output safe? */\n safe: true,\n /* Index of renderer in `output_area.display_order` */\n index: 0\n });\n }\n\n // register the mime type if in Jupyter Notebook environment and previously unregistered\n if (root.Jupyter !== undefined) {\n const events = require('base/js/events');\n const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n }\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"
    \\n\"+\n \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n \"
  • use INLINE resources instead, as so:
  • \\n\"+\n \"
\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded(error = null) {\n const el = document.getElementById(\"c3162abc-872f-4139-9799-b08236b47f64\");\n if (el != null) {\n const html = (() => {\n if (typeof root.Bokeh === \"undefined\") {\n if (error == null) {\n return \"BokehJS is loading ...\";\n } else {\n return \"BokehJS failed to load.\";\n }\n } else {\n const prefix = `BokehJS ${root.Bokeh.version}`;\n if (error == null) {\n return `${prefix} successfully loaded.`;\n } else {\n return `${prefix} encountered errors while loading and may not function as expected.`;\n }\n }\n })();\n el.innerHTML = html;\n\n if (error != null) {\n const wrapper = document.createElement(\"div\");\n wrapper.style.overflow = \"auto\";\n wrapper.style.height = \"5em\";\n wrapper.style.resize = \"vertical\";\n const content = document.createElement(\"div\");\n content.style.fontFamily = \"monospace\";\n content.style.whiteSpace = \"pre-wrap\";\n content.style.backgroundColor = \"rgb(255, 221, 221)\";\n content.textContent = error.stack ?? error.toString();\n wrapper.append(content);\n el.append(wrapper);\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(() => display_loaded(error), 100);\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.7.3.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n try {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n\n } catch (error) {display_loaded(error);throw error;\n }if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"c3162abc-872f-4139-9799-b08236b47f64\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));", + "application/vnd.bokehjs_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[get_engine] Creating engine for DB: master_config (base: master_config), PID: 62000\n", + "[get_engine] Creating engine for DB: portfolio_data_test (base: portfolio_data), PID: 62000\n", + "Fetching rates data from yfinance directly during market hours\n", + "[get_engine] Creating engine for DB: portfolio_config_long_bbands (base: portfolio_config), PID: 62000\n", + "[get_engine] Creating engine for DB: portfolio_data_long_bbands (base: portfolio_data), PID: 62000\n", + "2026-02-01 01:10:29 [test] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n", + "2026-02-01 01:10:31 [test] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n", + "2026-02-01 01:10:31 [test] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n", + "2026-02-01 01:10:32 [test] algo.strategies.init_strategies INFO: Loading timeseries data from 2025-03-07 to 2026-02-01 for stocks: ['AAPL', 'NVDA', 'TSLA', 'COST', 'AMZN', 'META', 'AMD', 'SBUX', 'NFLX', 'BA']\n", + "2026-02-01 01:10:32 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:32 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol AAPL not loaded. Loading now.\n", + "2026-02-01 01:10:37 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:37 [test] algo.strategies.init_strategies ERROR: Dataset for AAPL does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:37 [test] algo.strategies.init_strategies INFO: Formatted columns for AAPL: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:37 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:37 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol NVDA not loaded. Loading now.\n", + "2026-02-01 01:10:40 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:40 [test] algo.strategies.init_strategies ERROR: Dataset for NVDA does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:40 [test] algo.strategies.init_strategies INFO: Formatted columns for NVDA: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:40 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:40 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol TSLA not loaded. Loading now.\n", + "2026-02-01 01:10:43 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol TSLA\n", + "2026-02-01 01:10:44 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:44 [test] algo.strategies.init_strategies ERROR: Dataset for TSLA does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:44 [test] algo.strategies.init_strategies INFO: Formatted columns for TSLA: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:44 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:44 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol COST not loaded. Loading now.\n", + "2026-02-01 01:10:48 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:48 [test] algo.strategies.init_strategies ERROR: Dataset for COST does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:48 [test] algo.strategies.init_strategies INFO: Formatted columns for COST: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:48 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:48 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol AMZN not loaded. Loading now.\n", + "2026-02-01 01:10:50 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol AMZN\n", + "2026-02-01 01:10:51 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:51 [test] algo.strategies.init_strategies ERROR: Dataset for AMZN does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:51 [test] algo.strategies.init_strategies INFO: Formatted columns for AMZN: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:51 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:51 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol META not loaded. Loading now.\n", + "2026-02-01 01:10:53 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:54 [test] algo.strategies.init_strategies ERROR: Dataset for META does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:54 [test] algo.strategies.init_strategies INFO: Formatted columns for META: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:54 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:54 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol AMD not loaded. Loading now.\n", + "2026-02-01 01:10:57 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol AMD\n", + "2026-02-01 01:10:57 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:57 [test] algo.strategies.init_strategies ERROR: Dataset for AMD does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:57 [test] algo.strategies.init_strategies INFO: Formatted columns for AMD: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:57 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:57 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:10:57 [test] algo.strategies.init_strategies ERROR: Dataset for SBUX does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:10:57 [test] algo.strategies.init_strategies INFO: Formatted columns for SBUX: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:10:57 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:10:57 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol NFLX not loaded. Loading now.\n", + "2026-02-01 01:11:00 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol NFLX\n", + "2026-02-01 01:11:00 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:11:00 [test] algo.strategies.init_strategies ERROR: Dataset for NFLX does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:11:00 [test] algo.strategies.init_strategies INFO: Formatted columns for NFLX: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:11:00 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-02-01 01:10:32.634057 is today or in the future and current time is before market close. Forcing preload check.\n", + "2026-02-01 01:11:00 [test] EventDriven.riskmanager.market_data CRITICAL: Timeseries for symbol BA not loaded. Loading now.\n", + "2026-02-01 01:11:04 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 01:11:05 [test] algo.strategies.init_strategies ERROR: Dataset for BA does not contain the current time 2026-02-01 01:10:32.634057\n", + "2026-02-01 01:11:05 [test] algo.strategies.init_strategies INFO: Formatted columns for BA: ['Open', 'High', 'Low', 'Close', 'Volume']\n", + "2026-02-01 01:11:05 [test] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n" + ] + } + ], + "source": [ + "from dbase.database.db_utils import set_environment_context\n", + "from algo.strategies.init_strategies.new_system import get_multi_asset_instance\n", + "from algo.positions.vars import alpaca_client\n", + "set_environment_context(\"long_bbands\")\n", + "instance = get_multi_asset_instance(\"long_bbands\")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "de7277d7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'BA': ,\n", + " 'AMD': ,\n", + " 'AAPL': ,\n", + " 'AMZN': ,\n", + " 'COST': ,\n", + " 'META': ,\n", + " 'NFLX': ,\n", + " 'NVDA': ,\n", + " 'SBUX': ,\n", + " 'TSLA': }" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "instance.asset_strategies" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "71876fee", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "close": { + "bdata": "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", + "dtype": "f8" + }, + "high": { + "bdata": "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", + "dtype": "f8" + }, + "low": { + "bdata": "9QtSOvXhWkC4laQPmFtaQNC7RyFyL1pArt7mQAI3XED34V4SPXFcQKaLNlc6iF1AQ+urlYyAXUAPuEXlOqFcQGct2hgt6lxApYlUILocXUBGg6oHitlcQD7hqqdf1F1ARS1eo3+5XUAUKT5/ISxcQPbL+on0qFtAYRrCiDZDW0BXI1lwZehZQGZ9AvHXnFpA2eSOy1GxWkB4MjVYOGVZQBj8Vmr4BVdAFhrThaymVUBuscW4V5xXQNcP7XXJYFhAwALAyXLIWEA+m2PCeN1aQLgNmYA2Q1tA8e0ObreeW0D/nY8iohtZQNyEfagJAllAPjNLz3TBV0D8jd8pylBYQJYPL00YgFlAe8d6n9fFWUC2JSUSfm1aQP6xhm4MgFpA5JYCc+naWkDHZExE6QNaQFsc+kvo0VtAqFXxG11WXEDIfCdz7ihcQBqX03Uxs1tAPxXMtZ0QXEAYXaMEDvVcQMBDNAgazFxAT+6P1oUQXkDm7jB7ohxfQJbGQbH+dGBA1/EWelmUYEBgYSsC8q1gQGAKvPq1i2BAsc+93RGTYEArrKUMH1JgQOr6NgfWcGBAHBJmyl4kYEB3XrhBJalgQD2bnEd/2GBApeQFx1E8YUDDxwUJq5xgQHXqm3ED7GBAbHPVPJk9YUD4zdkpeHBhQLfqfvnAWWFA+sfyb3+vYUDQWI2GQb1hQKlmvVwjsGFAQBSfwVS7YUCNcFsEsbphQNdCTeexmmFAwj1L3uLlYUBnI/KvcfhhQGmH7YpT42FAWtcpuknUYUBf9ARXc8BhQLrgcVR6L2JAqsyG4sinYkDPR9KPcj9jQBhqaknDZ2NAHI/zPCl+Y0AjYAwpI+9iQGlG+M19HmNAeF3mEBO4Y0DvDX3TUKpjQACjWXrpy2NAV0sm1IokZEAXIRnJ8DJkQDVdtjR0bmRAhIjLIQ9AZEDlXp4AyyVlQOlsOMQxHGVAZOzCnfJZZUASHzmutGdlQJS0QhljX2VAkId0X/iRZEAzIUMUcP5kQAOwcV38aGVAIWninhmeZUBOuEuNar5lQJIQKDMD4GVAqJTBFqYAZkCgv9EHIf1lQCJwLfndW2VA0rfrpwPQZUByIo9KK/xlQJK5KileB2ZA/0Edb/VYZkBhzZ4cJ4xmQDgzWYlah2ZAwl1ypRNuZkByIZOOjmpmQBnvupkTbmZA1ylGNqRAZkBCXMptO5JmQHSjQAMN72VAh5Dirv4YZUD6k/LyS7llQHdBgzvJZWVAAAIfeZsRZkAi4qpXR1lmQCNJqsuOYmZAQCuE03wMZkA9OAbSLaRlQOUrfcRw5mRAaw6TGI4bZUBbjXxQgyxlQIbH7/SmgWRAfNiXoZnqZEBL7q4QFddkQGutAjVp7mVAuzuCsAsPZkDMsAHgFQ5mQMJ8jjoC0GVAjBdfetnLZUCLr2L70QxlQFOB8H5pnmVA9wcTjnLlZUDk2T6vaNZlQAbkLwNoBmZAhcxkt3zsZUDoDiQU2qNlQGHsibxy3WVALFKAUuuJZkBQdKpTCa9mQLPD3uV4/GZADZPhwJWBZ0CGyrNl1CtnQFrzCsI76mZAVGZmGaz/ZkCCRhGM8lBnQK4sZlCU4WdADWWLoUbBZkDPH4CTYz5nQK+siWYUdmZAJmCAyPYoZkALrw3uUXhmQDj81hSud2ZA0+AvSgm3ZkD3tsqeR3lmQPFsHzsBGGZAhZHdlPV4ZkA6YHBCrO9mQCbhcIVsjWdA6eMcSMf8Z0COaLlnmJhpQJRHvOzCLGlA9MYMUeFBaUB+aye6jbFpQI8NvjhovWhAQ7FcAXRUaECR1moG1EtnQH1iDjbNXGZAEDnrRe84aEC70jxZQulnQDRSsdfR42dA2BiMX9/6ZkASyer/PJJmQNbGRGrpCWdAaFm9x3p0ZkB/wh7vO9pmQM5kOTzhemZAWRkOl3OdZUCUT6akCw9mQNdZ30VMMWVAmvjzy1xHZkAxPmV6rw9mQA4BDFhztWVAn65x6q1/ZkB/OwV4M2NmQAAAAGC4fmZAAAAAwB6dZkAAAADAzMxmQAAAAIA96mZAAAAAoEfBZkAAAAAA1xNmQAAAAADX02VAAAAAwPXgZUAAAADAzNxlQAAAAIDrSWVAAAAAgD16ZUAAAABA4QpmQAAAAEAzy2ZAAAAAwMzcZkAAAABA4VJnQAAAAAAAgGdAAAAAwB49Z0AAAACAwl1nQAAAACCuT2dAAAAA4FGIZ0AAAADAzERnQAAAAIA9WmdAAAAAgOtRZ0AAAABguPZmQAAAAKBw9WZAAAAA4KPgZkAAAADAzOxmQAAAAKCZmWZAAAAAYI9KZ0AAAABgj0JnQAAAACCFM2ZAAAAAwMxMZkAAAACAwv1mQAAAAIA9WmdAAAAAIK4/Z0AAAABgZjZnQAAAAEDhumdAAAAAgOtBZ0AAAABACq9nQA==", + "dtype": "f8" + }, + "name": "Price", + "open": { + "bdata": "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", + "dtype": "f8" + }, + "type": "candlestick", + "x": [ + "2025-03-07T00:00:00", + "2025-03-10T00:00:00", + "2025-03-11T00:00:00", + "2025-03-12T00:00:00", + "2025-03-13T00:00:00", + "2025-03-14T00:00:00", + "2025-03-17T00:00:00", + "2025-03-18T00:00:00", + "2025-03-19T00:00:00", + "2025-03-20T00:00:00", + "2025-03-21T00:00:00", + "2025-03-24T00:00:00", + "2025-03-25T00:00:00", + "2025-03-26T00:00:00", + "2025-03-27T00:00:00", + "2025-03-28T00:00:00", + "2025-03-31T00:00:00", + "2025-04-01T00:00:00", + "2025-04-02T00:00:00", + "2025-04-03T00:00:00", + "2025-04-04T00:00:00", + "2025-04-07T00:00:00", + "2025-04-08T00:00:00", + "2025-04-09T00:00:00", + "2025-04-10T00:00:00", + "2025-04-11T00:00:00", + "2025-04-14T00:00:00", + "2025-04-15T00:00:00", + "2025-04-16T00:00:00", + "2025-04-17T00:00:00", + "2025-04-21T00:00:00", + "2025-04-22T00:00:00", + "2025-04-23T00:00:00", + "2025-04-24T00:00:00", + "2025-04-25T00:00:00", + "2025-04-28T00:00:00", + "2025-04-29T00:00:00", + "2025-04-30T00:00:00", + "2025-05-01T00:00:00", + "2025-05-02T00:00:00", + "2025-05-05T00:00:00", + "2025-05-06T00:00:00", + "2025-05-07T00:00:00", + "2025-05-08T00:00:00", + "2025-05-09T00:00:00", + "2025-05-12T00:00:00", + "2025-05-13T00:00:00", + "2025-05-14T00:00:00", + "2025-05-15T00:00:00", + "2025-05-16T00:00:00", + "2025-05-19T00:00:00", + "2025-05-20T00:00:00", + "2025-05-21T00:00:00", + "2025-05-22T00:00:00", + "2025-05-23T00:00:00", + "2025-05-27T00:00:00", + "2025-05-28T00:00:00", + "2025-05-29T00:00:00", + "2025-05-30T00:00:00", + "2025-06-02T00:00:00", + "2025-06-03T00:00:00", + "2025-06-04T00:00:00", + "2025-06-05T00:00:00", + "2025-06-06T00:00:00", + "2025-06-09T00:00:00", + "2025-06-10T00:00:00", + "2025-06-11T00:00:00", + "2025-06-12T00:00:00", + "2025-06-13T00:00:00", + "2025-06-16T00:00:00", + "2025-06-17T00:00:00", + "2025-06-18T00:00:00", + "2025-06-20T00:00:00", + "2025-06-23T00:00:00", + "2025-06-24T00:00:00", + "2025-06-25T00:00:00", + "2025-06-26T00:00:00", + "2025-06-27T00:00:00", + "2025-06-30T00:00:00", + "2025-07-01T00:00:00", + "2025-07-02T00:00:00", + "2025-07-03T00:00:00", + "2025-07-07T00:00:00", + "2025-07-08T00:00:00", + "2025-07-09T00:00:00", + "2025-07-10T00:00:00", + "2025-07-11T00:00:00", + "2025-07-14T00:00:00", + "2025-07-15T00:00:00", + "2025-07-16T00:00:00", + "2025-07-17T00:00:00", + "2025-07-18T00:00:00", + "2025-07-21T00:00:00", + "2025-07-22T00:00:00", + "2025-07-23T00:00:00", + "2025-07-24T00:00:00", + "2025-07-25T00:00:00", + "2025-07-28T00:00:00", + "2025-07-29T00:00:00", + "2025-07-30T00:00:00", + "2025-07-31T00:00:00", + "2025-08-01T00:00:00", + "2025-08-04T00:00:00", + "2025-08-05T00:00:00", + "2025-08-06T00:00:00", + "2025-08-07T00:00:00", + "2025-08-08T00:00:00", + "2025-08-11T00:00:00", + "2025-08-12T00:00:00", + "2025-08-13T00:00:00", + "2025-08-14T00:00:00", + "2025-08-15T00:00:00", + "2025-08-18T00:00:00", + "2025-08-19T00:00:00", + "2025-08-20T00:00:00", + "2025-08-21T00:00:00", + "2025-08-22T00:00:00", + "2025-08-25T00:00:00", + "2025-08-26T00:00:00", + "2025-08-27T00:00:00", + "2025-08-28T00:00:00", + "2025-08-29T00:00:00", + "2025-09-02T00:00:00", + "2025-09-03T00:00:00", + "2025-09-04T00:00:00", + "2025-09-05T00:00:00", + "2025-09-08T00:00:00", + "2025-09-09T00:00:00", + "2025-09-10T00:00:00", + "2025-09-11T00:00:00", + "2025-09-12T00:00:00", + "2025-09-15T00:00:00", + "2025-09-16T00:00:00", + "2025-09-17T00:00:00", + "2025-09-18T00:00:00", + "2025-09-19T00:00:00", + "2025-09-22T00:00:00", + "2025-09-23T00:00:00", + "2025-09-24T00:00:00", + "2025-09-25T00:00:00", + "2025-09-26T00:00:00", + "2025-09-29T00:00:00", + "2025-09-30T00:00:00", + "2025-10-01T00:00:00", + "2025-10-02T00:00:00", + "2025-10-03T00:00:00", + "2025-10-06T00:00:00", + "2025-10-07T00:00:00", + "2025-10-08T00:00:00", + "2025-10-09T00:00:00", + "2025-10-10T00:00:00", + "2025-10-13T00:00:00", + "2025-10-14T00:00:00", + "2025-10-15T00:00:00", + "2025-10-16T00:00:00", + "2025-10-17T00:00:00", + "2025-10-20T00:00:00", + "2025-10-21T00:00:00", + "2025-10-22T00:00:00", + "2025-10-23T00:00:00", + "2025-10-24T00:00:00", + "2025-10-27T00:00:00", + "2025-10-28T00:00:00", + "2025-10-29T00:00:00", + "2025-10-30T00:00:00", + "2025-10-31T00:00:00", + "2025-11-03T00:00:00", + "2025-11-04T00:00:00", + "2025-11-05T00:00:00", + "2025-11-06T00:00:00", + "2025-11-07T00:00:00", + "2025-11-10T00:00:00", + "2025-11-11T00:00:00", + "2025-11-12T00:00:00", + "2025-11-13T00:00:00", + "2025-11-14T00:00:00", + "2025-11-17T00:00:00", + "2025-11-18T00:00:00", + "2025-11-19T00:00:00", + "2025-11-20T00:00:00", + "2025-11-21T00:00:00", + "2025-11-24T00:00:00", + "2025-11-25T00:00:00", + "2025-11-26T00:00:00", + "2025-11-28T00:00:00", + "2025-12-01T00:00:00", + "2025-12-02T00:00:00", + "2025-12-03T00:00:00", + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00" + ], + "xaxis": "x", + "yaxis": "y" + }, + { + "line": { + "color": "blue" + }, + "mode": "lines", + "name": "middle_band", + "type": "scatter", + "x": [ + "2025-03-07T00:00:00", + "2025-03-10T00:00:00", + "2025-03-11T00:00:00", + "2025-03-12T00:00:00", + "2025-03-13T00:00:00", + "2025-03-14T00:00:00", + "2025-03-17T00:00:00", + "2025-03-18T00:00:00", + "2025-03-19T00:00:00", + "2025-03-20T00:00:00", + "2025-03-21T00:00:00", + "2025-03-24T00:00:00", + "2025-03-25T00:00:00", + "2025-03-26T00:00:00", + "2025-03-27T00:00:00", + "2025-03-28T00:00:00", + "2025-03-31T00:00:00", + "2025-04-01T00:00:00", + "2025-04-02T00:00:00", + "2025-04-03T00:00:00", + "2025-04-04T00:00:00", + "2025-04-07T00:00:00", + "2025-04-08T00:00:00", + "2025-04-09T00:00:00", + "2025-04-10T00:00:00", + "2025-04-11T00:00:00", + "2025-04-14T00:00:00", + "2025-04-15T00:00:00", + "2025-04-16T00:00:00", + "2025-04-17T00:00:00", + "2025-04-21T00:00:00", + "2025-04-22T00:00:00", + "2025-04-23T00:00:00", + "2025-04-24T00:00:00", + "2025-04-25T00:00:00", + "2025-04-28T00:00:00", + "2025-04-29T00:00:00", + "2025-04-30T00:00:00", + "2025-05-01T00:00:00", + "2025-05-02T00:00:00", + "2025-05-05T00:00:00", + "2025-05-06T00:00:00", + "2025-05-07T00:00:00", + "2025-05-08T00:00:00", + "2025-05-09T00:00:00", + "2025-05-12T00:00:00", + "2025-05-13T00:00:00", + "2025-05-14T00:00:00", + "2025-05-15T00:00:00", + "2025-05-16T00:00:00", + "2025-05-19T00:00:00", + "2025-05-20T00:00:00", + "2025-05-21T00:00:00", + "2025-05-22T00:00:00", + "2025-05-23T00:00:00", + "2025-05-27T00:00:00", + "2025-05-28T00:00:00", + "2025-05-29T00:00:00", + "2025-05-30T00:00:00", + "2025-06-02T00:00:00", + "2025-06-03T00:00:00", + "2025-06-04T00:00:00", + "2025-06-05T00:00:00", + "2025-06-06T00:00:00", + "2025-06-09T00:00:00", + "2025-06-10T00:00:00", + "2025-06-11T00:00:00", + "2025-06-12T00:00:00", + "2025-06-13T00:00:00", + "2025-06-16T00:00:00", + "2025-06-17T00:00:00", + "2025-06-18T00:00:00", + "2025-06-20T00:00:00", + "2025-06-23T00:00:00", + "2025-06-24T00:00:00", + "2025-06-25T00:00:00", + "2025-06-26T00:00:00", + "2025-06-27T00:00:00", + "2025-06-30T00:00:00", + "2025-07-01T00:00:00", + "2025-07-02T00:00:00", + "2025-07-03T00:00:00", + "2025-07-07T00:00:00", + "2025-07-08T00:00:00", + "2025-07-09T00:00:00", + "2025-07-10T00:00:00", + "2025-07-11T00:00:00", + "2025-07-14T00:00:00", + "2025-07-15T00:00:00", + "2025-07-16T00:00:00", + "2025-07-17T00:00:00", + "2025-07-18T00:00:00", + "2025-07-21T00:00:00", + "2025-07-22T00:00:00", + "2025-07-23T00:00:00", + "2025-07-24T00:00:00", + "2025-07-25T00:00:00", + "2025-07-28T00:00:00", + "2025-07-29T00:00:00", + "2025-07-30T00:00:00", + "2025-07-31T00:00:00", + "2025-08-01T00:00:00", + "2025-08-04T00:00:00", + "2025-08-05T00:00:00", + "2025-08-06T00:00:00", + "2025-08-07T00:00:00", + "2025-08-08T00:00:00", + "2025-08-11T00:00:00", + "2025-08-12T00:00:00", + "2025-08-13T00:00:00", + "2025-08-14T00:00:00", + "2025-08-15T00:00:00", + "2025-08-18T00:00:00", + "2025-08-19T00:00:00", + "2025-08-20T00:00:00", + "2025-08-21T00:00:00", + "2025-08-22T00:00:00", + "2025-08-25T00:00:00", + "2025-08-26T00:00:00", + "2025-08-27T00:00:00", + "2025-08-28T00:00:00", + "2025-08-29T00:00:00", + "2025-09-02T00:00:00", + "2025-09-03T00:00:00", + "2025-09-04T00:00:00", + "2025-09-05T00:00:00", + "2025-09-08T00:00:00", + "2025-09-09T00:00:00", + "2025-09-10T00:00:00", + "2025-09-11T00:00:00", + "2025-09-12T00:00:00", + "2025-09-15T00:00:00", + "2025-09-16T00:00:00", + "2025-09-17T00:00:00", + "2025-09-18T00:00:00", + "2025-09-19T00:00:00", + "2025-09-22T00:00:00", + "2025-09-23T00:00:00", + "2025-09-24T00:00:00", + "2025-09-25T00:00:00", + "2025-09-26T00:00:00", + "2025-09-29T00:00:00", + "2025-09-30T00:00:00", + "2025-10-01T00:00:00", + "2025-10-02T00:00:00", + "2025-10-03T00:00:00", + "2025-10-06T00:00:00", + "2025-10-07T00:00:00", + "2025-10-08T00:00:00", + "2025-10-09T00:00:00", + "2025-10-10T00:00:00", + "2025-10-13T00:00:00", + "2025-10-14T00:00:00", + "2025-10-15T00:00:00", + "2025-10-16T00:00:00", + "2025-10-17T00:00:00", + "2025-10-20T00:00:00", + "2025-10-21T00:00:00", + "2025-10-22T00:00:00", + "2025-10-23T00:00:00", + "2025-10-24T00:00:00", + "2025-10-27T00:00:00", + "2025-10-28T00:00:00", + "2025-10-29T00:00:00", + "2025-10-30T00:00:00", + "2025-10-31T00:00:00", + "2025-11-03T00:00:00", + "2025-11-04T00:00:00", + "2025-11-05T00:00:00", + "2025-11-06T00:00:00", + "2025-11-07T00:00:00", + "2025-11-10T00:00:00", + "2025-11-11T00:00:00", + "2025-11-12T00:00:00", + "2025-11-13T00:00:00", + "2025-11-14T00:00:00", + "2025-11-17T00:00:00", + "2025-11-18T00:00:00", + "2025-11-19T00:00:00", + "2025-11-20T00:00:00", + "2025-11-21T00:00:00", + "2025-11-24T00:00:00", + "2025-11-25T00:00:00", + "2025-11-26T00:00:00", + "2025-11-28T00:00:00", + "2025-12-01T00:00:00", + "2025-12-02T00:00:00", + "2025-12-03T00:00:00", + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00" + ], + "xaxis": "x", + "y": { + "bdata": "AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/3RKn5j2NY0C6JU6lhJljQOLU74GopmNA8LklVkyzY0Cnfs/1R75jQBhpx6pLyGNAzy1xmn/RY0ArEEZyTdtjQHHq97Sn5GNAiVO/1zHuY0AbymvIt/hjQCsQRurVA2RAA2GkHUIPZEBpxwp8sxpkQLfEqV+iJ2RA2FBeG5I0ZEAN5TVksEFkQOLU73HZTmRAQ3kNhRtcZECPFQh7MmlkQPjcEheXd2RAhJF2NI+HZEDi1O9JSJZkQBvKa2AzpWRAjLRjrRixZEBLnPrlRr5kQEk7VihxymRA8xrKe03XZEB/zy05xuNkQHesQIAr8GRAS5z6dfT9ZEDFqd9jxAxlQMWp3/u4G2VAoryGKtUpZUBc4tRfqTdlQK+hvH45RWVAHisQllZTZUCMtGOtK2FlQA==", + "dtype": "f8" + }, + "yaxis": "y" + }, + { + "line": { + "color": "green" + }, + "mode": "lines", + "name": "upper_band", + "type": "scatter", + "x": [ + "2025-03-07T00:00:00", + "2025-03-10T00:00:00", + "2025-03-11T00:00:00", + "2025-03-12T00:00:00", + "2025-03-13T00:00:00", + "2025-03-14T00:00:00", + "2025-03-17T00:00:00", + "2025-03-18T00:00:00", + "2025-03-19T00:00:00", + "2025-03-20T00:00:00", + "2025-03-21T00:00:00", + "2025-03-24T00:00:00", + "2025-03-25T00:00:00", + "2025-03-26T00:00:00", + "2025-03-27T00:00:00", + "2025-03-28T00:00:00", + "2025-03-31T00:00:00", + "2025-04-01T00:00:00", + "2025-04-02T00:00:00", + "2025-04-03T00:00:00", + "2025-04-04T00:00:00", + "2025-04-07T00:00:00", + "2025-04-08T00:00:00", + "2025-04-09T00:00:00", + "2025-04-10T00:00:00", + "2025-04-11T00:00:00", + "2025-04-14T00:00:00", + "2025-04-15T00:00:00", + "2025-04-16T00:00:00", + "2025-04-17T00:00:00", + "2025-04-21T00:00:00", + "2025-04-22T00:00:00", + "2025-04-23T00:00:00", + "2025-04-24T00:00:00", + "2025-04-25T00:00:00", + "2025-04-28T00:00:00", + "2025-04-29T00:00:00", + "2025-04-30T00:00:00", + "2025-05-01T00:00:00", + "2025-05-02T00:00:00", + "2025-05-05T00:00:00", + "2025-05-06T00:00:00", + "2025-05-07T00:00:00", + "2025-05-08T00:00:00", + "2025-05-09T00:00:00", + "2025-05-12T00:00:00", + "2025-05-13T00:00:00", + "2025-05-14T00:00:00", + "2025-05-15T00:00:00", + "2025-05-16T00:00:00", + "2025-05-19T00:00:00", + "2025-05-20T00:00:00", + "2025-05-21T00:00:00", + "2025-05-22T00:00:00", + "2025-05-23T00:00:00", + "2025-05-27T00:00:00", + "2025-05-28T00:00:00", + "2025-05-29T00:00:00", + "2025-05-30T00:00:00", + "2025-06-02T00:00:00", + "2025-06-03T00:00:00", + "2025-06-04T00:00:00", + "2025-06-05T00:00:00", + "2025-06-06T00:00:00", + "2025-06-09T00:00:00", + "2025-06-10T00:00:00", + "2025-06-11T00:00:00", + "2025-06-12T00:00:00", + "2025-06-13T00:00:00", + "2025-06-16T00:00:00", + "2025-06-17T00:00:00", + "2025-06-18T00:00:00", + "2025-06-20T00:00:00", + "2025-06-23T00:00:00", + "2025-06-24T00:00:00", + "2025-06-25T00:00:00", + "2025-06-26T00:00:00", + "2025-06-27T00:00:00", + "2025-06-30T00:00:00", + "2025-07-01T00:00:00", + "2025-07-02T00:00:00", + "2025-07-03T00:00:00", + "2025-07-07T00:00:00", + "2025-07-08T00:00:00", + "2025-07-09T00:00:00", + "2025-07-10T00:00:00", + "2025-07-11T00:00:00", + "2025-07-14T00:00:00", + "2025-07-15T00:00:00", + "2025-07-16T00:00:00", + "2025-07-17T00:00:00", + "2025-07-18T00:00:00", + "2025-07-21T00:00:00", + "2025-07-22T00:00:00", + "2025-07-23T00:00:00", + "2025-07-24T00:00:00", + "2025-07-25T00:00:00", + "2025-07-28T00:00:00", + "2025-07-29T00:00:00", + "2025-07-30T00:00:00", + "2025-07-31T00:00:00", + "2025-08-01T00:00:00", + "2025-08-04T00:00:00", + "2025-08-05T00:00:00", + "2025-08-06T00:00:00", + "2025-08-07T00:00:00", + "2025-08-08T00:00:00", + "2025-08-11T00:00:00", + "2025-08-12T00:00:00", + "2025-08-13T00:00:00", + "2025-08-14T00:00:00", + "2025-08-15T00:00:00", + "2025-08-18T00:00:00", + "2025-08-19T00:00:00", + "2025-08-20T00:00:00", + "2025-08-21T00:00:00", + "2025-08-22T00:00:00", + "2025-08-25T00:00:00", + "2025-08-26T00:00:00", + "2025-08-27T00:00:00", + "2025-08-28T00:00:00", + "2025-08-29T00:00:00", + "2025-09-02T00:00:00", + "2025-09-03T00:00:00", + "2025-09-04T00:00:00", + "2025-09-05T00:00:00", + "2025-09-08T00:00:00", + "2025-09-09T00:00:00", + "2025-09-10T00:00:00", + "2025-09-11T00:00:00", + "2025-09-12T00:00:00", + "2025-09-15T00:00:00", + "2025-09-16T00:00:00", + "2025-09-17T00:00:00", + "2025-09-18T00:00:00", + "2025-09-19T00:00:00", + "2025-09-22T00:00:00", + "2025-09-23T00:00:00", + "2025-09-24T00:00:00", + "2025-09-25T00:00:00", + "2025-09-26T00:00:00", + "2025-09-29T00:00:00", + "2025-09-30T00:00:00", + "2025-10-01T00:00:00", + "2025-10-02T00:00:00", + "2025-10-03T00:00:00", + "2025-10-06T00:00:00", + "2025-10-07T00:00:00", + "2025-10-08T00:00:00", + "2025-10-09T00:00:00", + "2025-10-10T00:00:00", + "2025-10-13T00:00:00", + "2025-10-14T00:00:00", + "2025-10-15T00:00:00", + "2025-10-16T00:00:00", + "2025-10-17T00:00:00", + "2025-10-20T00:00:00", + "2025-10-21T00:00:00", + "2025-10-22T00:00:00", + "2025-10-23T00:00:00", + "2025-10-24T00:00:00", + "2025-10-27T00:00:00", + "2025-10-28T00:00:00", + "2025-10-29T00:00:00", + "2025-10-30T00:00:00", + "2025-10-31T00:00:00", + "2025-11-03T00:00:00", + "2025-11-04T00:00:00", + "2025-11-05T00:00:00", + "2025-11-06T00:00:00", + "2025-11-07T00:00:00", + "2025-11-10T00:00:00", + "2025-11-11T00:00:00", + "2025-11-12T00:00:00", + "2025-11-13T00:00:00", + "2025-11-14T00:00:00", + "2025-11-17T00:00:00", + "2025-11-18T00:00:00", + "2025-11-19T00:00:00", + "2025-11-20T00:00:00", + "2025-11-21T00:00:00", + "2025-11-24T00:00:00", + "2025-11-25T00:00:00", + "2025-11-26T00:00:00", + "2025-11-28T00:00:00", + "2025-12-01T00:00:00", + "2025-12-02T00:00:00", + "2025-12-03T00:00:00", + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00" + ], + "xaxis": "x", + "y": { + "bdata": "AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/3xef9XjvY0A6HqrKcvtjQMxhXvscCGRAqv/WDUoUZECtyQ7c7R5kQBj9BZCEKGRAP0PDeG4xZEAH5QS75zpkQCrxFdfCQ2RAj4Zt/91MZED8Pvn/CVdkQEGdsIjOYWRAbNCI5ghtZEA1bDgNPnhkQI9s++bHhGRAySoc/zKRZECfOjUGt51kQBKtrrcxqmRA3y96ida2ZEDWCUmNScNkQAMAfB600GRAQVn9s2LfZEBe7S3M2+xkQHcfNVZs+mRADtDmF50FZUDpMYJt1hFlQFOxs4gbHWVAzq85nRUpZUBrzG3SrzRlQK3MPKrLP2VAHdiXPSpMZUDZUR4ETlllQMzbzVioZmVAxvnNtkZzZUCOcL2Kw39lQGHSW0UxjGVAxlGdTgmZZUD15cG0iaVlQA==", + "dtype": "f8" + }, + "yaxis": "y" + }, + { + "line": { + "color": "red" + }, + "mode": "lines", + "name": "lower_band", + "type": "scatter", + "x": [ + "2025-03-07T00:00:00", + "2025-03-10T00:00:00", + "2025-03-11T00:00:00", + "2025-03-12T00:00:00", + "2025-03-13T00:00:00", + "2025-03-14T00:00:00", + "2025-03-17T00:00:00", + "2025-03-18T00:00:00", + "2025-03-19T00:00:00", + "2025-03-20T00:00:00", + "2025-03-21T00:00:00", + "2025-03-24T00:00:00", + "2025-03-25T00:00:00", + "2025-03-26T00:00:00", + "2025-03-27T00:00:00", + "2025-03-28T00:00:00", + "2025-03-31T00:00:00", + "2025-04-01T00:00:00", + "2025-04-02T00:00:00", + "2025-04-03T00:00:00", + "2025-04-04T00:00:00", + "2025-04-07T00:00:00", + "2025-04-08T00:00:00", + "2025-04-09T00:00:00", + "2025-04-10T00:00:00", + "2025-04-11T00:00:00", + "2025-04-14T00:00:00", + "2025-04-15T00:00:00", + "2025-04-16T00:00:00", + "2025-04-17T00:00:00", + "2025-04-21T00:00:00", + "2025-04-22T00:00:00", + "2025-04-23T00:00:00", + "2025-04-24T00:00:00", + "2025-04-25T00:00:00", + "2025-04-28T00:00:00", + "2025-04-29T00:00:00", + "2025-04-30T00:00:00", + "2025-05-01T00:00:00", + "2025-05-02T00:00:00", + "2025-05-05T00:00:00", + "2025-05-06T00:00:00", + "2025-05-07T00:00:00", + "2025-05-08T00:00:00", + "2025-05-09T00:00:00", + "2025-05-12T00:00:00", + "2025-05-13T00:00:00", + "2025-05-14T00:00:00", + "2025-05-15T00:00:00", + "2025-05-16T00:00:00", + "2025-05-19T00:00:00", + "2025-05-20T00:00:00", + "2025-05-21T00:00:00", + "2025-05-22T00:00:00", + "2025-05-23T00:00:00", + "2025-05-27T00:00:00", + "2025-05-28T00:00:00", + "2025-05-29T00:00:00", + "2025-05-30T00:00:00", + "2025-06-02T00:00:00", + "2025-06-03T00:00:00", + "2025-06-04T00:00:00", + "2025-06-05T00:00:00", + "2025-06-06T00:00:00", + "2025-06-09T00:00:00", + "2025-06-10T00:00:00", + "2025-06-11T00:00:00", + "2025-06-12T00:00:00", + "2025-06-13T00:00:00", + "2025-06-16T00:00:00", + "2025-06-17T00:00:00", + "2025-06-18T00:00:00", + "2025-06-20T00:00:00", + "2025-06-23T00:00:00", + "2025-06-24T00:00:00", + "2025-06-25T00:00:00", + "2025-06-26T00:00:00", + "2025-06-27T00:00:00", + "2025-06-30T00:00:00", + "2025-07-01T00:00:00", + "2025-07-02T00:00:00", + "2025-07-03T00:00:00", + "2025-07-07T00:00:00", + "2025-07-08T00:00:00", + "2025-07-09T00:00:00", + "2025-07-10T00:00:00", + "2025-07-11T00:00:00", + "2025-07-14T00:00:00", + "2025-07-15T00:00:00", + "2025-07-16T00:00:00", + "2025-07-17T00:00:00", + "2025-07-18T00:00:00", + "2025-07-21T00:00:00", + "2025-07-22T00:00:00", + "2025-07-23T00:00:00", + "2025-07-24T00:00:00", + "2025-07-25T00:00:00", + "2025-07-28T00:00:00", + "2025-07-29T00:00:00", + "2025-07-30T00:00:00", + "2025-07-31T00:00:00", + "2025-08-01T00:00:00", + "2025-08-04T00:00:00", + "2025-08-05T00:00:00", + "2025-08-06T00:00:00", + "2025-08-07T00:00:00", + "2025-08-08T00:00:00", + "2025-08-11T00:00:00", + "2025-08-12T00:00:00", + "2025-08-13T00:00:00", + "2025-08-14T00:00:00", + "2025-08-15T00:00:00", + "2025-08-18T00:00:00", + "2025-08-19T00:00:00", + "2025-08-20T00:00:00", + "2025-08-21T00:00:00", + "2025-08-22T00:00:00", + "2025-08-25T00:00:00", + "2025-08-26T00:00:00", + "2025-08-27T00:00:00", + "2025-08-28T00:00:00", + "2025-08-29T00:00:00", + "2025-09-02T00:00:00", + "2025-09-03T00:00:00", + "2025-09-04T00:00:00", + "2025-09-05T00:00:00", + "2025-09-08T00:00:00", + "2025-09-09T00:00:00", + "2025-09-10T00:00:00", + "2025-09-11T00:00:00", + "2025-09-12T00:00:00", + "2025-09-15T00:00:00", + "2025-09-16T00:00:00", + "2025-09-17T00:00:00", + "2025-09-18T00:00:00", + "2025-09-19T00:00:00", + "2025-09-22T00:00:00", + "2025-09-23T00:00:00", + "2025-09-24T00:00:00", + "2025-09-25T00:00:00", + "2025-09-26T00:00:00", + "2025-09-29T00:00:00", + "2025-09-30T00:00:00", + "2025-10-01T00:00:00", + "2025-10-02T00:00:00", + "2025-10-03T00:00:00", + "2025-10-06T00:00:00", + "2025-10-07T00:00:00", + "2025-10-08T00:00:00", + "2025-10-09T00:00:00", + "2025-10-10T00:00:00", + "2025-10-13T00:00:00", + "2025-10-14T00:00:00", + "2025-10-15T00:00:00", + "2025-10-16T00:00:00", + "2025-10-17T00:00:00", + "2025-10-20T00:00:00", + "2025-10-21T00:00:00", + "2025-10-22T00:00:00", + "2025-10-23T00:00:00", + "2025-10-24T00:00:00", + "2025-10-27T00:00:00", + "2025-10-28T00:00:00", + "2025-10-29T00:00:00", + "2025-10-30T00:00:00", + "2025-10-31T00:00:00", + "2025-11-03T00:00:00", + "2025-11-04T00:00:00", + "2025-11-05T00:00:00", + "2025-11-06T00:00:00", + "2025-11-07T00:00:00", + "2025-11-10T00:00:00", + "2025-11-11T00:00:00", + "2025-11-12T00:00:00", + "2025-11-13T00:00:00", + "2025-11-14T00:00:00", + "2025-11-17T00:00:00", + "2025-11-18T00:00:00", + "2025-11-19T00:00:00", + "2025-11-20T00:00:00", + "2025-11-21T00:00:00", + "2025-11-24T00:00:00", + "2025-11-25T00:00:00", + "2025-11-26T00:00:00", + "2025-11-28T00:00:00", + "2025-12-01T00:00:00", + "2025-12-02T00:00:00", + "2025-12-03T00:00:00", + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00" + ], + "xaxis": "x", + "y": { + "bdata": "AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/AAAAAAAA+H8AAAAAAAD4fwAAAAAAAPh/2w2v1wIrY0A6LfJ/ljdjQPhHgQg0RWNANnR0nk5SY0ChM5APol1jQBjViMUSaGNAXxgfvJBxY0BPO4cps3tjQLjj2ZKMhWNAgyARsIWPY0A6Vd6QZZpjQBWD20vdpWNAmvG/VHuxY0CdIt3qKL1jQN8cWNh8ymNA53agN/HXY0B7jzbCqeVjQLL8MCyB82NAp8KggGABZEBIIcdoGw9kQO25qQ96HmRAx8nvtLsvZEBmvLHHtD9kQL90omr6T2RACpngQpRcZECtBnNet2pkQD/F+MfGd2RAGIZaWoWFZECT0u2f3JJkQEGMRFaLoGRAeWBdrr6vZECxAaHDOsBkQL538Z7J0GRAfn8/nmPgZEAqVOw0j+9kQP1wHbhB/mRAdgSD3aMNZUAjgwWmzRxlQA==", + "dtype": "f8" + }, + "yaxis": "y" + }, + { + "line": { + "color": "purple" + }, + "mode": "lines", + "name": "adx", + "type": "scatter", + "x": [ + "2025-03-07T00:00:00", + "2025-03-10T00:00:00", + "2025-03-11T00:00:00", + "2025-03-12T00:00:00", + "2025-03-13T00:00:00", + "2025-03-14T00:00:00", + "2025-03-17T00:00:00", + "2025-03-18T00:00:00", + "2025-03-19T00:00:00", + "2025-03-20T00:00:00", + "2025-03-21T00:00:00", + "2025-03-24T00:00:00", + "2025-03-25T00:00:00", + "2025-03-26T00:00:00", + "2025-03-27T00:00:00", + "2025-03-28T00:00:00", + "2025-03-31T00:00:00", + "2025-04-01T00:00:00", + "2025-04-02T00:00:00", + "2025-04-03T00:00:00", + "2025-04-04T00:00:00", + "2025-04-07T00:00:00", + "2025-04-08T00:00:00", + "2025-04-09T00:00:00", + "2025-04-10T00:00:00", + "2025-04-11T00:00:00", + "2025-04-14T00:00:00", + "2025-04-15T00:00:00", + "2025-04-16T00:00:00", + "2025-04-17T00:00:00", + "2025-04-21T00:00:00", + "2025-04-22T00:00:00", + "2025-04-23T00:00:00", + "2025-04-24T00:00:00", + "2025-04-25T00:00:00", + "2025-04-28T00:00:00", + "2025-04-29T00:00:00", + "2025-04-30T00:00:00", + "2025-05-01T00:00:00", + "2025-05-02T00:00:00", + "2025-05-05T00:00:00", + "2025-05-06T00:00:00", + "2025-05-07T00:00:00", + "2025-05-08T00:00:00", + "2025-05-09T00:00:00", + "2025-05-12T00:00:00", + "2025-05-13T00:00:00", + "2025-05-14T00:00:00", + "2025-05-15T00:00:00", + "2025-05-16T00:00:00", + "2025-05-19T00:00:00", + "2025-05-20T00:00:00", + "2025-05-21T00:00:00", + "2025-05-22T00:00:00", + "2025-05-23T00:00:00", + "2025-05-27T00:00:00", + "2025-05-28T00:00:00", + "2025-05-29T00:00:00", + "2025-05-30T00:00:00", + "2025-06-02T00:00:00", + "2025-06-03T00:00:00", + "2025-06-04T00:00:00", + "2025-06-05T00:00:00", + "2025-06-06T00:00:00", + "2025-06-09T00:00:00", + "2025-06-10T00:00:00", + "2025-06-11T00:00:00", + "2025-06-12T00:00:00", + "2025-06-13T00:00:00", + "2025-06-16T00:00:00", + "2025-06-17T00:00:00", + "2025-06-18T00:00:00", + "2025-06-20T00:00:00", + "2025-06-23T00:00:00", + "2025-06-24T00:00:00", + "2025-06-25T00:00:00", + "2025-06-26T00:00:00", + "2025-06-27T00:00:00", + "2025-06-30T00:00:00", + "2025-07-01T00:00:00", + "2025-07-02T00:00:00", + "2025-07-03T00:00:00", + "2025-07-07T00:00:00", + "2025-07-08T00:00:00", + "2025-07-09T00:00:00", + "2025-07-10T00:00:00", + "2025-07-11T00:00:00", + "2025-07-14T00:00:00", + "2025-07-15T00:00:00", + "2025-07-16T00:00:00", + "2025-07-17T00:00:00", + "2025-07-18T00:00:00", + "2025-07-21T00:00:00", + "2025-07-22T00:00:00", + "2025-07-23T00:00:00", + "2025-07-24T00:00:00", + "2025-07-25T00:00:00", + "2025-07-28T00:00:00", + "2025-07-29T00:00:00", + "2025-07-30T00:00:00", + "2025-07-31T00:00:00", + "2025-08-01T00:00:00", + "2025-08-04T00:00:00", + "2025-08-05T00:00:00", + "2025-08-06T00:00:00", + "2025-08-07T00:00:00", + "2025-08-08T00:00:00", + "2025-08-11T00:00:00", + "2025-08-12T00:00:00", + "2025-08-13T00:00:00", + "2025-08-14T00:00:00", + "2025-08-15T00:00:00", + "2025-08-18T00:00:00", + "2025-08-19T00:00:00", + "2025-08-20T00:00:00", + "2025-08-21T00:00:00", + "2025-08-22T00:00:00", + "2025-08-25T00:00:00", + "2025-08-26T00:00:00", + "2025-08-27T00:00:00", + "2025-08-28T00:00:00", + "2025-08-29T00:00:00", + "2025-09-02T00:00:00", + "2025-09-03T00:00:00", + "2025-09-04T00:00:00", + "2025-09-05T00:00:00", + "2025-09-08T00:00:00", + "2025-09-09T00:00:00", + "2025-09-10T00:00:00", + "2025-09-11T00:00:00", + "2025-09-12T00:00:00", + "2025-09-15T00:00:00", + "2025-09-16T00:00:00", + "2025-09-17T00:00:00", + "2025-09-18T00:00:00", + "2025-09-19T00:00:00", + "2025-09-22T00:00:00", + "2025-09-23T00:00:00", + "2025-09-24T00:00:00", + "2025-09-25T00:00:00", + "2025-09-26T00:00:00", + "2025-09-29T00:00:00", + "2025-09-30T00:00:00", + "2025-10-01T00:00:00", + "2025-10-02T00:00:00", + "2025-10-03T00:00:00", + "2025-10-06T00:00:00", + "2025-10-07T00:00:00", + "2025-10-08T00:00:00", + "2025-10-09T00:00:00", + "2025-10-10T00:00:00", + "2025-10-13T00:00:00", + "2025-10-14T00:00:00", + "2025-10-15T00:00:00", + "2025-10-16T00:00:00", + "2025-10-17T00:00:00", + "2025-10-20T00:00:00", + "2025-10-21T00:00:00", + "2025-10-22T00:00:00", + "2025-10-23T00:00:00", + "2025-10-24T00:00:00", + "2025-10-27T00:00:00", + "2025-10-28T00:00:00", + "2025-10-29T00:00:00", + "2025-10-30T00:00:00", + "2025-10-31T00:00:00", + "2025-11-03T00:00:00", + "2025-11-04T00:00:00", + "2025-11-05T00:00:00", + "2025-11-06T00:00:00", + "2025-11-07T00:00:00", + "2025-11-10T00:00:00", + "2025-11-11T00:00:00", + "2025-11-12T00:00:00", + "2025-11-13T00:00:00", + "2025-11-14T00:00:00", + "2025-11-17T00:00:00", + "2025-11-18T00:00:00", + "2025-11-19T00:00:00", + "2025-11-20T00:00:00", + "2025-11-21T00:00:00", + "2025-11-24T00:00:00", + "2025-11-25T00:00:00", + "2025-11-26T00:00:00", + "2025-11-28T00:00:00", + "2025-12-01T00:00:00", + "2025-12-02T00:00:00", + "2025-12-03T00:00:00", + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00" + ], + "xaxis": "x2", + "y": { + "bdata": "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", + "dtype": "f8" + }, + "yaxis": "y2" + }, + { + "line": { + "color": "orange" + }, + "mode": "lines", + "name": "atr", + "type": "scatter", + "x": [ + "2025-03-07T00:00:00", + "2025-03-10T00:00:00", + "2025-03-11T00:00:00", + "2025-03-12T00:00:00", + "2025-03-13T00:00:00", + "2025-03-14T00:00:00", + "2025-03-17T00:00:00", + "2025-03-18T00:00:00", + "2025-03-19T00:00:00", + "2025-03-20T00:00:00", + "2025-03-21T00:00:00", + "2025-03-24T00:00:00", + "2025-03-25T00:00:00", + "2025-03-26T00:00:00", + "2025-03-27T00:00:00", + "2025-03-28T00:00:00", + "2025-03-31T00:00:00", + "2025-04-01T00:00:00", + "2025-04-02T00:00:00", + "2025-04-03T00:00:00", + "2025-04-04T00:00:00", + "2025-04-07T00:00:00", + "2025-04-08T00:00:00", + "2025-04-09T00:00:00", + "2025-04-10T00:00:00", + "2025-04-11T00:00:00", + "2025-04-14T00:00:00", + "2025-04-15T00:00:00", + "2025-04-16T00:00:00", + "2025-04-17T00:00:00", + "2025-04-21T00:00:00", + "2025-04-22T00:00:00", + "2025-04-23T00:00:00", + "2025-04-24T00:00:00", + "2025-04-25T00:00:00", + "2025-04-28T00:00:00", + "2025-04-29T00:00:00", + "2025-04-30T00:00:00", + "2025-05-01T00:00:00", + "2025-05-02T00:00:00", + "2025-05-05T00:00:00", + "2025-05-06T00:00:00", + "2025-05-07T00:00:00", + "2025-05-08T00:00:00", + "2025-05-09T00:00:00", + "2025-05-12T00:00:00", + "2025-05-13T00:00:00", + "2025-05-14T00:00:00", + "2025-05-15T00:00:00", + "2025-05-16T00:00:00", + "2025-05-19T00:00:00", + "2025-05-20T00:00:00", + "2025-05-21T00:00:00", + "2025-05-22T00:00:00", + "2025-05-23T00:00:00", + "2025-05-27T00:00:00", + "2025-05-28T00:00:00", + "2025-05-29T00:00:00", + "2025-05-30T00:00:00", + "2025-06-02T00:00:00", + "2025-06-03T00:00:00", + "2025-06-04T00:00:00", + "2025-06-05T00:00:00", + "2025-06-06T00:00:00", + "2025-06-09T00:00:00", + "2025-06-10T00:00:00", + "2025-06-11T00:00:00", + "2025-06-12T00:00:00", + "2025-06-13T00:00:00", + "2025-06-16T00:00:00", + "2025-06-17T00:00:00", + "2025-06-18T00:00:00", + "2025-06-20T00:00:00", + "2025-06-23T00:00:00", + "2025-06-24T00:00:00", + "2025-06-25T00:00:00", + "2025-06-26T00:00:00", + "2025-06-27T00:00:00", + "2025-06-30T00:00:00", + "2025-07-01T00:00:00", + "2025-07-02T00:00:00", + "2025-07-03T00:00:00", + "2025-07-07T00:00:00", + "2025-07-08T00:00:00", + "2025-07-09T00:00:00", + "2025-07-10T00:00:00", + "2025-07-11T00:00:00", + "2025-07-14T00:00:00", + "2025-07-15T00:00:00", + "2025-07-16T00:00:00", + "2025-07-17T00:00:00", + "2025-07-18T00:00:00", + "2025-07-21T00:00:00", + "2025-07-22T00:00:00", + "2025-07-23T00:00:00", + "2025-07-24T00:00:00", + "2025-07-25T00:00:00", + "2025-07-28T00:00:00", + "2025-07-29T00:00:00", + "2025-07-30T00:00:00", + "2025-07-31T00:00:00", + "2025-08-01T00:00:00", + "2025-08-04T00:00:00", + "2025-08-05T00:00:00", + "2025-08-06T00:00:00", + "2025-08-07T00:00:00", + "2025-08-08T00:00:00", + "2025-08-11T00:00:00", + "2025-08-12T00:00:00", + "2025-08-13T00:00:00", + "2025-08-14T00:00:00", + "2025-08-15T00:00:00", + "2025-08-18T00:00:00", + "2025-08-19T00:00:00", + "2025-08-20T00:00:00", + "2025-08-21T00:00:00", + "2025-08-22T00:00:00", + "2025-08-25T00:00:00", + "2025-08-26T00:00:00", + "2025-08-27T00:00:00", + "2025-08-28T00:00:00", + "2025-08-29T00:00:00", + "2025-09-02T00:00:00", + "2025-09-03T00:00:00", + "2025-09-04T00:00:00", + "2025-09-05T00:00:00", + "2025-09-08T00:00:00", + "2025-09-09T00:00:00", + "2025-09-10T00:00:00", + "2025-09-11T00:00:00", + "2025-09-12T00:00:00", + "2025-09-15T00:00:00", + "2025-09-16T00:00:00", + "2025-09-17T00:00:00", + "2025-09-18T00:00:00", + "2025-09-19T00:00:00", + "2025-09-22T00:00:00", + "2025-09-23T00:00:00", + "2025-09-24T00:00:00", + "2025-09-25T00:00:00", + "2025-09-26T00:00:00", + "2025-09-29T00:00:00", + "2025-09-30T00:00:00", + "2025-10-01T00:00:00", + "2025-10-02T00:00:00", + "2025-10-03T00:00:00", + "2025-10-06T00:00:00", + "2025-10-07T00:00:00", + "2025-10-08T00:00:00", + "2025-10-09T00:00:00", + "2025-10-10T00:00:00", + "2025-10-13T00:00:00", + "2025-10-14T00:00:00", + "2025-10-15T00:00:00", + "2025-10-16T00:00:00", + "2025-10-17T00:00:00", + "2025-10-20T00:00:00", + "2025-10-21T00:00:00", + "2025-10-22T00:00:00", + "2025-10-23T00:00:00", + "2025-10-24T00:00:00", + "2025-10-27T00:00:00", + "2025-10-28T00:00:00", + "2025-10-29T00:00:00", + "2025-10-30T00:00:00", + "2025-10-31T00:00:00", + "2025-11-03T00:00:00", + "2025-11-04T00:00:00", + "2025-11-05T00:00:00", + "2025-11-06T00:00:00", + "2025-11-07T00:00:00", + "2025-11-10T00:00:00", + "2025-11-11T00:00:00", + "2025-11-12T00:00:00", + "2025-11-13T00:00:00", + "2025-11-14T00:00:00", + "2025-11-17T00:00:00", + "2025-11-18T00:00:00", + "2025-11-19T00:00:00", + "2025-11-20T00:00:00", + "2025-11-21T00:00:00", + "2025-11-24T00:00:00", + "2025-11-25T00:00:00", + "2025-11-26T00:00:00", + "2025-11-28T00:00:00", + "2025-12-01T00:00:00", + "2025-12-02T00:00:00", + "2025-12-03T00:00:00", + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00" + ], + "xaxis": "x3", + "y": { + "bdata": "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", + "dtype": "f8" + }, + "yaxis": "y3" + } + ], + "layout": { + "height": 900, + "showlegend": true, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermap": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermap" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "title": { + "text": "Strategy Indicators for LongBBandsTrend_SL" + }, + "width": 1000, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "matches": "x3", + "rangebreaks": [ + { + "bounds": [ + "sat", + "mon" + ] + } + ], + "rangeslider": { + "visible": false + }, + "showticklabels": false + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0, + 1 + ], + "matches": "x3", + "rangebreaks": [ + { + "bounds": [ + "sat", + "mon" + ] + } + ], + "showticklabels": false + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0, + 1 + ], + "rangebreaks": [ + { + "bounds": [ + "sat", + "mon" + ] + } + ] + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0.424, + 1 + ], + "title": { + "text": "Price" + }, + "type": "linear" + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.212, + 0.404 + ] + }, + "yaxis3": { + "anchor": "x3", + "domain": [ + 0, + 0.192 + ] + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "instance.get_strategy(\"NVDA\").plot_strategy_indicators(False)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/notebooks/test_load_data.ipynb b/trade/datamanager/notebooks/test_load_data.ipynb new file mode 100644 index 0000000..ee1a0fd --- /dev/null +++ b/trade/datamanager/notebooks/test_load_data.ipynb @@ -0,0 +1,2138 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 15, + "id": "34fe7ce8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n", + "2026-02-02 14:22:53 trade.helpers.Logging INFO: Logging Root Directory: /Users/chiemelienwanisobi/cloned_repos/QuantTools/logs\n", + "2026-02-02 14:22:53 [test] trade.helpers.clear_cache INFO: No expired caches to delete on 2026-02-02.\n", + "2026-02-02 14:22:57 [test] dbase.DataAPI.ThetaData.proxy INFO: Refreshed proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-02 14:22:57 [test] dbase.DataAPI.ThetaData.proxy INFO: Using Proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-02 14:22:57 [test] dbase.DataAPI.ThetaData INFO: Using V2 of the ThetaData API\n", + "\n", + "\n", + "Scheduled Data Requests will be saved to: /Users/chiemelienwanisobi/cloned_repos/QuantTools/module_test/raw_code/DataManagers/scheduler/requests.jsonl\n", + "2026-02-02 14:23:00 [test] DataManager.py CRITICAL: Using ProcessSaveManager for saving data.\n", + "Fetching rates data from yfinance directly during market hours\n" + ] + }, + { + "data": { + "text/plain": [ + "set()" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from trade.datamanager import (\n", + " DividendDataManager,\n", + " SpotDataManager,\n", + " OptionSpotDataManager,\n", + " VolDataManager,\n", + " RatesDataManager,\n", + " BaseDataManager,\n", + " ForwardDataManager,\n", + " GreekDataManager,\n", + " assert_synchronized_model,\n", + " get_option_theoretical_price,\n", + " calculate_scenarios,\n", + ")\n", + "\n", + "from trade.datamanager._enums import (\n", + " OptionSpotEndpointSource,\n", + " SeriesId,\n", + " OptionPricingModel,\n", + " VolatilityModel,\n", + " RealTimeFallbackOption,\n", + " GreekType,\n", + " ModelPrice,\n", + ")\n", + "from trade.optionlib.config.types import DivType\n", + "from trade.helpers.helper_types import SingletonMetaClass\n", + "from trade.datamanager.vars import get_loaded_names, TS\n", + "from trade.datamanager.utils.model import LoadRequest, _load_model_data_timeseries\n", + "from trade.datamanager.utils.logging import (\n", + " change_datamanager_factor_loggers_level,\n", + " change_datamanager_utils_logging_level,\n", + " change_logging_in_all_datamanager_loggers,\n", + " change_all_optionlib_loggers_level,\n", + ")\n", + "\n", + "# change_datamanager_factor_loggers_level(\"CRITICAL\")\n", + "# change_datamanager_utils_logging_level(\"CRITICAL\")\n", + "# change_logging_in_all_datamanager_loggers(\"CRITICAL\")\n", + "change_all_optionlib_loggers_level(\"CRITICAL\")\n", + "get_loaded_names()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "c8cde08a", + "metadata": {}, + "outputs": [], + "source": [ + "## Vars\n", + "div = DivType.CONTINUOUS\n", + "undo_adjust = True\n", + "endpoint_source = OptionSpotEndpointSource.EOD\n", + "series_id = SeriesId.HIST\n", + "market_model = OptionPricingModel.BSM\n", + "vol_model = VolatilityModel.MARKET\n", + "model_price = ModelPrice.ASK\n", + "\n", + "symbol = \"SBUX\"\n", + "expiration = \"2026-09-18\"\n", + "right = \"C\"\n", + "strike = 100.0\n", + "ts_start = \"2025-01-01\"\n", + "ts_end = \"2026-01-28\"" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9b108219", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 14:23:04 [test] trade.datamanager.vars INFO: Loading timeseries for SBUX...\n", + "2026-02-02 14:23:07 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:07 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 14:23:07 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 14:23:08 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 14:23:08 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 to 2026-01-28...\n", + "2026-02-02 14:23:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:08 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 14:23:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:08 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:08 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 to 2026-01-28...\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 14:23:08 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-02 14:23:08 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 14:23:08 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-28 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-28 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 14:23:08 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n" + ] + } + ], + "source": [ + "request = LoadRequest(\n", + " symbol=symbol,\n", + " # start_date=ts_start,\n", + " # end_date=ts_end,\n", + " as_of=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " series_id=SeriesId.HIST,\n", + " dividend_type=DivType.DISCRETE,\n", + " endpoint_source=OptionSpotEndpointSource.EOD,\n", + " vol_model=VolatilityModel.MARKET,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " model_price=ModelPrice.ASK,\n", + " load_spot=True,\n", + " load_dividend=True,\n", + " load_forward=True,\n", + " load_option_spot=True,\n", + " load_vol=True,\n", + " load_greek=True,\n", + " load_rates=True,\n", + " undo_adjust=True,\n", + " # rt=True,\n", + ")\n", + "data_packet = _load_model_data_timeseries(request)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "a8aa6806", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data_packet.greek.fallback_option\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "ea740571", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Timestamp('2026-01-28 00:00:00')" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "request.end_date" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "03b201a3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 14:23:09 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:09 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:09 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:09 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 14:23:09 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 14:23:09 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 14:23:09 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 14:23:11 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D to avoid saving partial day data.\n", + "2026-02-02 14:23:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 14:23:11 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:12 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol SBUX.\n", + "2026-02-02 14:23:14 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 14:23:15 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 14:23:15 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 14:23:15 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 14:23:15 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 14:23:15 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 14:23:15 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Exception ignored on calling ctypes callback function: \n", + "Traceback (most recent call last):\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/llvmlite/binding/executionengine.py\", line 178, in _raw_object_cache_notify\n", + " def _raw_object_cache_notify(self, data):\n", + "\n", + "KeyboardInterrupt: \n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 14:23:18 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 14:23:18 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n" + ] + } + ], + "source": [ + "bsm = calculate_scenarios(\n", + " symbol=symbol,\n", + " expiration=expiration,\n", + " right=right,\n", + " strike=strike,\n", + " return_pnl_in_pct=True,\n", + " return_pnl=True,\n", + " rt=True\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a648a8c7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:44:46 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:46 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:44:46.443119. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:44:46 for 2026-02-02 as fallback.\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2026-02-02 0.03575\n", + "Name: annualized, dtype: float64" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RatesDataManager().rt().timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "90745ffc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 14:23:37 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 14:23:37 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol SBUX.\n", + "2026-02-02 14:23:39 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n" + ] + }, + { + "data": { + "text/plain": [ + "2026-02-02 91.860001\n", + "Name: close, dtype: object" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "SpotDataManager(\"SBUX\").rt().timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "637c49ef", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[*********************100%***********************] 1 of 1 completed\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
PriceCloseHighLowOpenVolume
TickerSBUXSBUXSBUXSBUXSBUX
Date
2026-01-0283.97000184.95999983.01999784.22000111334900
2026-01-0586.55999887.25000083.40000283.5000009894300
2026-01-0689.45999990.63999986.36000186.55999811351100
2026-01-0786.69000289.37999786.47000189.0199979547000
2026-01-0888.18000089.29000185.61000185.8600018140100
2026-01-0988.87999789.09999886.79000188.3499987593200
2026-01-1289.95999990.23000387.59999888.4000027289500
2026-01-1390.55999890.59999889.47000189.6900025561900
2026-01-1491.15000291.51999790.15000290.2600028373200
2026-01-1593.27999994.16999891.63999992.07000010001700
2026-01-1692.98999893.66999892.22000193.3600018958200
2026-01-2093.66000493.80999890.77999991.76000213555200
2026-01-2196.43000096.51999793.73000393.82000017118100
2026-01-2295.83000297.80000394.86000196.47000114150800
2026-01-2397.62000397.88999995.76000297.09999811759800
2026-01-2696.33000297.97000195.79000197.20999914106700
2026-01-2795.72000196.65000295.23999895.76999716403600
2026-01-2895.160004104.82000095.099998102.30000325961600
2026-01-2993.87999796.90000292.61000196.76000216602900
2026-01-3091.94999793.20999991.00000092.72000110744300
2026-02-0291.86499892.61000190.62999791.8399965419473
\n", + "
" + ], + "text/plain": [ + "Price Close High Low Open Volume\n", + "Ticker SBUX SBUX SBUX SBUX SBUX\n", + "Date \n", + "2026-01-02 83.970001 84.959999 83.019997 84.220001 11334900\n", + "2026-01-05 86.559998 87.250000 83.400002 83.500000 9894300\n", + "2026-01-06 89.459999 90.639999 86.360001 86.559998 11351100\n", + "2026-01-07 86.690002 89.379997 86.470001 89.019997 9547000\n", + "2026-01-08 88.180000 89.290001 85.610001 85.860001 8140100\n", + "2026-01-09 88.879997 89.099998 86.790001 88.349998 7593200\n", + "2026-01-12 89.959999 90.230003 87.599998 88.400002 7289500\n", + "2026-01-13 90.559998 90.599998 89.470001 89.690002 5561900\n", + "2026-01-14 91.150002 91.519997 90.150002 90.260002 8373200\n", + "2026-01-15 93.279999 94.169998 91.639999 92.070000 10001700\n", + "2026-01-16 92.989998 93.669998 92.220001 93.360001 8958200\n", + "2026-01-20 93.660004 93.809998 90.779999 91.760002 13555200\n", + "2026-01-21 96.430000 96.519997 93.730003 93.820000 17118100\n", + "2026-01-22 95.830002 97.800003 94.860001 96.470001 14150800\n", + "2026-01-23 97.620003 97.889999 95.760002 97.099998 11759800\n", + "2026-01-26 96.330002 97.970001 95.790001 97.209999 14106700\n", + "2026-01-27 95.720001 96.650002 95.239998 95.769997 16403600\n", + "2026-01-28 95.160004 104.820000 95.099998 102.300003 25961600\n", + "2026-01-29 93.879997 96.900002 92.610001 96.760002 16602900\n", + "2026-01-30 91.949997 93.209999 91.000000 92.720001 10744300\n", + "2026-02-02 91.864998 92.610001 90.629997 91.839996 5419473" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import yfinance as yf\n", + "yf.download(\n", + " tickers=\"SBUX\",\n", + " start=\"2026-01-01\",\n", + " # end=\"2024-06-01\",\n", + " end=\"2026-02-03\",\n", + " interval=\"1d\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "dbd23464", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
OpenHighLowCloseVolumeDividendsStock Splits
Date
2025-02-03228.954495230.786206224.683801226.983398730633000.00.0
2025-02-04226.226817232.080347225.629512231.751831450673000.00.0
2025-02-05227.501062231.622421227.242238231.423325396203000.00.0
2025-02-06230.248634232.747343229.392506232.169952299253000.00.0
2025-02-07231.552758232.946448226.236789226.605133397072000.00.0
........................
2026-01-27259.170013261.950012258.209991258.269989496483000.00.0
2026-01-28257.649994258.859985254.509995256.440002412880000.00.0
2026-01-29258.000000259.649994254.410004258.279999672530000.00.0
2026-01-30255.169998261.899994252.179993259.480011923526000.00.0
2026-02-02260.019989270.489990259.204987270.010010728900960.00.0
\n", + "

251 rows × 7 columns

\n", + "
" + ], + "text/plain": [ + " Open High Low Close Volume \\\n", + "Date \n", + "2025-02-03 228.954495 230.786206 224.683801 226.983398 73063300 \n", + "2025-02-04 226.226817 232.080347 225.629512 231.751831 45067300 \n", + "2025-02-05 227.501062 231.622421 227.242238 231.423325 39620300 \n", + "2025-02-06 230.248634 232.747343 229.392506 232.169952 29925300 \n", + "2025-02-07 231.552758 232.946448 226.236789 226.605133 39707200 \n", + "... ... ... ... ... ... \n", + "2026-01-27 259.170013 261.950012 258.209991 258.269989 49648300 \n", + "2026-01-28 257.649994 258.859985 254.509995 256.440002 41288000 \n", + "2026-01-29 258.000000 259.649994 254.410004 258.279999 67253000 \n", + "2026-01-30 255.169998 261.899994 252.179993 259.480011 92352600 \n", + "2026-02-02 260.019989 270.489990 259.204987 270.010010 72890096 \n", + "\n", + " Dividends Stock Splits \n", + "Date \n", + "2025-02-03 0.0 0.0 \n", + "2025-02-04 0.0 0.0 \n", + "2025-02-05 0.0 0.0 \n", + "2025-02-06 0.0 0.0 \n", + "2025-02-07 0.0 0.0 \n", + "... ... ... \n", + "2026-01-27 0.0 0.0 \n", + "2026-01-28 0.0 0.0 \n", + "2026-01-29 0.0 0.0 \n", + "2026-01-30 0.0 0.0 \n", + "2026-02-02 0.0 0.0 \n", + "\n", + "[251 rows x 7 columns]" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "t = yf.Ticker(\"AAPL\").history(\n", + " start=\"2025-02-01\",\n", + " end=\"2026-02-03\",\n", + " interval=\"1d\",\n", + " # progress=False,\n", + " # multi_level_index=False,\n", + ")\n", + "t.index = t.index.tz_localize(None)\n", + "t" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "720a78c2", + "metadata": {}, + "outputs": [], + "source": [ + "yf.Ticker(\"AAPL\").pr" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "f15aff46", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
namedescriptiondailyannualized
Datetime
2026-02-02^IRX13 WEEK TREASURY BILL0.0000960.03578
\n", + "
" + ], + "text/plain": [ + " name description daily annualized\n", + "Datetime \n", + "2026-02-02 ^IRX 13 WEEK TREASURY BILL 0.000096 0.03578" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "RatesDataManager()._query_yfinance(\n", + " start_date=\"2026-02-02\",\n", + " end_date=\"2026-02-02\",\n", + " interval=\"1d\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a9eae24", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0.900.951.001.051.10
-0.02-0.602754-0.382721-0.0977800.2552750.676231
-0.01-0.566061-0.339467-0.0488090.3085280.731857
0.00-0.529591-0.2964110.0000060.3616820.787452
0.01-0.493324-0.2535360.0486780.4147460.843016
0.02-0.457242-0.2108270.0972170.4677230.898549
\n", + "
" + ], + "text/plain": [ + " 0.90 0.95 1.00 1.05 1.10\n", + "-0.02 -0.602754 -0.382721 -0.097780 0.255275 0.676231\n", + "-0.01 -0.566061 -0.339467 -0.048809 0.308528 0.731857\n", + " 0.00 -0.529591 -0.296411 0.000006 0.361682 0.787452\n", + " 0.01 -0.493324 -0.253536 0.048678 0.414746 0.843016\n", + " 0.02 -0.457242 -0.210827 0.097217 0.467723 0.898549" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "bsm.grid" + ] + }, + { + "cell_type": "markdown", + "id": "dc6c936a", + "metadata": {}, + "source": [ + "## Batch Load Real Time" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee6c306d", + "metadata": {}, + "outputs": [], + "source": [ + "option_metas = [\n", + " {\n", + " \"symbol\": \"SBUX\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 100.0,\n", + " },\n", + " {\n", + " \"symbol\": \"SBUX\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 105.0,\n", + " },\n", + " {\n", + " \"symbol\": \"AAPL\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 260.0,\n", + " },\n", + " {\n", + " \"symbol\": \"AAPL\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 265.0,\n", + " },\n", + " {\n", + " \"symbol\": \"AMD\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 280.0,\n", + " },\n", + " {\n", + " \"symbol\": \"AMD\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 290.0,\n", + " },\n", + " {\n", + " \"symbol\": \"META\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 890.0,\n", + " },\n", + " {\n", + " \"symbol\": \"META\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 900.0,\n", + " }\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc83fa2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:44:48 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:48 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:48 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:48 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:44:48 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:48 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:44:50 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:44:50 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:44:48.761262. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:44:50 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:50 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:50 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:44:50 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:44:48 for 2026-02-02 as fallback.\n", + "2026-02-02 11:44:50 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:50 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol SBUX.\n", + "2026-02-02 11:44:52 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:44:52 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:44:52 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:44:52 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 11:44:52 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:44:52 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:44:52 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:44:52 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:44:52 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:44:52 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:52 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:52 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:52 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:44:52 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:44:52 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:52 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:44:54 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:44:54 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:44:52.925818. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:44:54 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:54 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:44:54 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:44:54 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:44:52 for 2026-02-02 as fallback.\n", + "2026-02-02 11:44:54 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:44:54 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol SBUX.\n", + "2026-02-02 11:44:56 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:44:56 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:44:56 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:44:56 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick SBUX20260918C105\n", + "2026-02-02 11:44:56 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:44:56 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:44:56 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:44:56 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:44:56 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:44:56 [test] trade.datamanager.vars INFO: Loading timeseries for AAPL...\n", + "2026-02-02 11:44:58 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:44:58 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:44:58 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:44:58 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:00 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:00 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:01 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:01 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:00.206707. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:01 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:01 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:01 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:01 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:00 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:01 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:02 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:02 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:02 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:45:02 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-02 11:45:02 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:02 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:02 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:02 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:02 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:02 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:02 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:02 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:02 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:02 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:02 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:02 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:04 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:04 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:02.623254. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:04 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:04 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:04 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:04 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:02 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:04 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:04 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol AAPL.\n", + "2026-02-02 11:45:06 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:06 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:06 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:45:06 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AAPL20260918C265\n", + "2026-02-02 11:45:06 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:06 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:06 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:06 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:06 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:06 [test] trade.datamanager.vars INFO: Loading timeseries for AMD...\n", + "2026-02-02 11:45:08 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol AMD\n", + "2026-02-02 11:45:08 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:08 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:08 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:08 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:09 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:09 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:09.018505. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:09 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:15 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:15 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:15 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C280\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:15 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:15 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:15 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:17 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:17 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:15.556395. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:17 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:17 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:17 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:17 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:15 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:17 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:17 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol AMD.\n", + "2026-02-02 11:45:19 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol AMD\n", + "2026-02-02 11:45:19 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:19 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:19 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:45:19 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C290\n", + "2026-02-02 11:45:19 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:19 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:19 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:19 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:19 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:19 [test] trade.datamanager.vars INFO: Loading timeseries for META...\n", + "2026-02-02 11:45:20 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:20 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:20 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:20 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:21 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:21 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:21.250107. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:21 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:23 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:23 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:23 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick META20260918C890\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:23 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:23 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:23 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:25 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:25 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:23.469167. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:25 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:25 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:25 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:25 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:23 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:25 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:25 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol META.\n", + "2026-02-02 11:45:26 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick META20260918C900\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n" + ] + } + ], + "source": [ + "full_data = []\n", + "for option_meta in option_metas:\n", + " option_scenarios = calculate_scenarios(\n", + " symbol=option_meta[\"symbol\"],\n", + " expiration=option_meta[\"expiration\"],\n", + " right=option_meta[\"right\"],\n", + " strike=option_meta[\"strike\"],\n", + " return_pnl_in_pct=True,\n", + " return_pnl=True,\n", + " rt=True\n", + " )\n", + " full_data.append(option_scenarios)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f1250b16", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:45:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:27 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:27 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:27 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:27 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:27 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:27 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:27 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:27 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:27 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:27 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:27 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:27 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:28 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C105\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:105\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:105\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C105\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C105\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for AAPL from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:28 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:28 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:28 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:28 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:28 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:260\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:260\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for AAPL from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:28 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:AAPL|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:28 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:28 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:28 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:28 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:28 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:29 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AAPL20260918C265\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:265\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:AAPL|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:265\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AAPL20260918C265\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AAPL20260918C265\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-06 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for AMD from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:AMD|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:AMD|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:29 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:29 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:29 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:29 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AMD20260918C280\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:280\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-07-14 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:AMD|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:280\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AMD20260918C280\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-07-14 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AMD20260918C280\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-07-14 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for AMD from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:AMD|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:AMD|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:29 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:29 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:29 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:29 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AMD20260918C290\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:290\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-07-14 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:AMD|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:290\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AMD20260918C290\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-07-14 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick AMD20260918C290\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-07-14 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:29 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market\n", + "2026-02-02 11:45:29 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for META from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:META|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:META|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:29 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:30 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:30 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:30 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:30 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:META|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick META20260918C890\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:META|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:890\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:META|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:890\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick META20260918C890\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick META20260918C890\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:30 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-02 11:45:30 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for META from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-02 11:45:30 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:META|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:30 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:META|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-02 11:45:30 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:30 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:30 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-02 11:45:30 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:45:30 [test] trade.datamanager.forward INFO: Cache hit for forward timeseries key: symbol:META|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick META20260918C900\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:META|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:900\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:META|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:900\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:30 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick META20260918C900\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick META20260918C900\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-02-02 11:45:30 [test] trade.datamanager.utils INFO: Cache hit for greeks timeseries key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market\n" + ] + } + ], + "source": [ + "timeseries_full_data = []\n", + "for option_meta in option_metas:\n", + " request = LoadRequest(\n", + " symbol=option_meta[\"symbol\"],\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=option_meta[\"expiration\"],\n", + " strike=option_meta[\"strike\"],\n", + " right=option_meta[\"right\"],\n", + " series_id=SeriesId.HIST,\n", + " dividend_type=DivType.DISCRETE,\n", + " endpoint_source=OptionSpotEndpointSource.EOD,\n", + " vol_model=VolatilityModel.MARKET,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " model_price=ModelPrice.ASK,\n", + " load_spot=True,\n", + " load_dividend=True,\n", + " load_forward=True,\n", + " load_option_spot=True,\n", + " load_vol=True,\n", + " load_greek=True,\n", + " load_rates=True,\n", + " undo_adjust=True,\n", + " # rt=True,\n", + " )\n", + " data_packet = _load_model_data_timeseries(request)\n", + " timeseries_full_data.append(data_packet)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42dcd29d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[ModelResultPack(symbol='SBUX', strike=100.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='SBUX', strike=105.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='AAPL', strike=260.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='AAPL', strike=265.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='AMD', strike=280.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='AMD', strike=290.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='META', strike=890.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0),\n", + " ModelResultPack(symbol='META', strike=900.0, expiration=datetime.datetime(2026, 9, 18, 0, 0), right='C', series_id=, dividend_type=, undo_adjust=True, num_empty=0)]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "timeseries_full_data\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ccfa0daf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:45:31 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:31 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:31 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:31 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:31 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:31 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:31 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:37 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:37 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:31.085799. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:37 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:37 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:37 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:37 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:31 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:37 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:37 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol SBUX.\n", + "2026-02-02 11:45:39 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:39 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:39 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:39 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:45:39.743402 - 2026-02-02 11:45:39.743402 and option tick SBUX20260918C100\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:39 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:40 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:40 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:40 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:40 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:40 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:40 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:40 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:40 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:40 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:42 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:42 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:40.298922. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:42 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:42 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:42 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:40 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:42 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:42 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol SBUX.\n", + "2026-02-02 11:45:44 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick SBUX20260918C105\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:45:44.651546 - 2026-02-02 11:45:44.651546 and option tick SBUX20260918C105\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:105|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:44 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:44 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:44 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:46 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:46 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:44.715522. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:46 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:46 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:46 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:46 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:44 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:46 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:46 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol AAPL.\n", + "2026-02-02 11:45:48 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AAPL20260918C260\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:45:48.549351 - 2026-02-02 11:45:48.549351 and option tick AAPL20260918C260\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:260|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:48 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:48 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:48 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:50 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:50 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:48.615803. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:50 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:50 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:50 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:50 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:48 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:50 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:50 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol AAPL.\n", + "2026-02-02 11:45:52 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AAPL20260918C265\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:45:52.592241 - 2026-02-02 11:45:52.592241 and option tick AAPL20260918C265\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AAPL|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:265|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:52 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:52 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:52 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:54 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:54 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:52.653355. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:54 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:54 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:54 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:54 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:52 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:54 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:54 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol AMD.\n", + "2026-02-02 11:45:55 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol AMD\n", + "2026-02-02 11:45:55 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C280\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:45:56.167799 - 2026-02-02 11:45:56.167799 and option tick AMD20260918C280\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:45:56 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:45:56 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:56 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:45:58 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:45:58 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:45:56.282274. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:45:58 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:58 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:45:58 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:45:58 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:45:56 for 2026-02-02 as fallback.\n", + "2026-02-02 11:45:58 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:45:58 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol AMD.\n", + "2026-02-02 11:45:59 [test] EventDriven.riskmanager.market_data ERROR: Failed to retrieve dividends for symbol AMD\n", + "2026-02-02 11:45:59 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C290\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:46:00.199268 - 2026-02-02 11:46:00.199268 and option tick AMD20260918C290\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:290|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:46:00 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:46:00 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:46:00 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:02 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:46:02 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:46:00.272098. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:46:02 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:46:02 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:46:02 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:46:02 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:46:00 for 2026-02-02 as fallback.\n", + "2026-02-02 11:46:02 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:02 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol META.\n", + "2026-02-02 11:46:06 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:META|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick META20260918C890\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:46:06.674915 - 2026-02-02 11:46:06.674915 and option tick META20260918C890\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:890|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-02-02 11:46:06 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:META|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-02-02 11:46:06 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:46:06 [test] trade.datamanager.rates INFO: Cache partially covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:08 [test] trade.datamanager.rates WARNING: No risk-free rate data found for date range 2026-02-02 00:00:00 to 2026-02-02 00:00:00.\n", + "2026-02-02 11:46:08 [test] trade.datamanager.rates WARNING: No rate data found for date 2026-02-02 11:46:06.739109. Resorting to fallback option `RealTimeFallbackOption.USE_LAST_AVAILABLE`.\n", + "2026-02-02 11:46:08 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:46:08 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-02 11:46:08 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-30 to 2026-01-30...\n", + "2026-02-02 11:46:08 [test] trade.datamanager.rates WARNING: Using last available rate for date 2026-01-30 11:46:06 for 2026-02-02 as fallback.\n", + "2026-02-02 11:46:08 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:08 [test] EventDriven.riskmanager.market_data CRITICAL: Reloading timeseries data for symbol META.\n", + "2026-02-02 11:46:09 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-02 11:46:10 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.vars INFO: Timeseries for META already loaded.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:META|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:10 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-02 11:46:10 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.ASK\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 00:00:00 - 2026-02-02 00:00:00 and option tick META20260918C900\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|n_steps:100|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Using cached date range for 2026-02-02 11:46:10.215226 - 2026-02-02 11:46:10.215226 and option tick META20260918C900\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market. Fetching missing dates: [Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market. Fetching missing dates.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:META|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:ask|option_pricing_model:Binomial|right:C|strike:900|volatility_model:market to avoid saving partial day data.\n", + "2026-02-02 11:46:10 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-02 to 2026-02-02...\n" + ] + } + ], + "source": [ + "rt_full_data = []\n", + "for option_meta in option_metas:\n", + " request = LoadRequest(\n", + " symbol=option_meta[\"symbol\"],\n", + "\n", + " expiration=option_meta[\"expiration\"],\n", + " strike=option_meta[\"strike\"],\n", + " right=option_meta[\"right\"],\n", + " series_id=SeriesId.HIST,\n", + " dividend_type=DivType.DISCRETE,\n", + " endpoint_source=OptionSpotEndpointSource.EOD,\n", + " vol_model=VolatilityModel.MARKET,\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " model_price=ModelPrice.ASK,\n", + " load_spot=True,\n", + " load_dividend=True,\n", + " load_forward=True,\n", + " load_option_spot=True,\n", + " load_vol=True,\n", + " load_greek=True,\n", + " load_rates=True,\n", + " undo_adjust=True,\n", + " rt=True,\n", + " )\n", + " data_packet = _load_model_data_timeseries(request)\n", + " rt_full_data.append(data_packet)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef3b9240", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2026-02-02 5.625\n", + "Name: midpoint, dtype: float64" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rt_full_data[0].option_spot.price" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0b070efc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2026-02-02 5.625\n", + "Name: midpoint, dtype: float64" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "OptionSpotDataManager(\"SBUX\").rt(\n", + " strike=100.0,\n", + " right=\"C\",\n", + " expiration=\"2026-09-18\",\n", + ").price" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d7dee52", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:46:10 [long_bbands] algo.__init__ CRITICAL: ALGO_DIR not on main branch; skipping runtime safeguards.\n", + "[get_engine] Creating engine for DB: master_config (base: master_config), PID: 8931\n", + "[get_engine] Creating engine for DB: portfolio_data_long_bbands (base: portfolio_data), PID: 8931\n" + ] + }, + { + "data": { + "text/html": [ + " \n", + "
\n", + " \n", + " Loading BokehJS ...\n", + "
\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": "'use strict';\n(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\nconst JS_MIME_TYPE = 'application/javascript';\n const HTML_MIME_TYPE = 'text/html';\n const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n const CLASS_NAME = 'output_bokeh rendered_html';\n\n /**\n * Render data to the DOM node\n */\n function render(props, node) {\n const script = document.createElement(\"script\");\n node.appendChild(script);\n }\n\n /**\n * Handle when an output is cleared or removed\n */\n function handleClearOutput(event, handle) {\n function drop(id) {\n const view = Bokeh.index.get_by_id(id)\n if (view != null) {\n view.model.document.clear()\n Bokeh.index.delete(view)\n }\n }\n\n const cell = handle.cell;\n\n const id = cell.output_area._bokeh_element_id;\n const server_id = cell.output_area._bokeh_server_id;\n\n // Clean up Bokeh references\n if (id != null) {\n drop(id)\n }\n\n if (server_id !== undefined) {\n // Clean up Bokeh references\n const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n cell.notebook.kernel.execute(cmd_clean, {\n iopub: {\n output: function(msg) {\n const id = msg.content.text.trim()\n drop(id)\n }\n }\n });\n // Destroy server and session\n const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n cell.notebook.kernel.execute(cmd_destroy);\n }\n }\n\n /**\n * Handle when a new output is added\n */\n function handleAddOutput(event, handle) {\n const output_area = handle.output_area;\n const output = handle.output;\n\n // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n return\n }\n\n const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n\n if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n // store reference to embed id on output_area\n output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n }\n if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n const bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n const script_attrs = bk_div.children[0].attributes;\n for (let i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n }\n\n function register_renderer(events, OutputArea) {\n\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n const toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[toinsert.length - 1]);\n element.append(toinsert);\n return toinsert\n }\n\n /* Handle when an output is cleared or removed */\n events.on('clear_output.CodeCell', handleClearOutput);\n events.on('delete.Cell', handleClearOutput);\n\n /* Handle when a new output is added */\n events.on('output_added.OutputArea', handleAddOutput);\n\n /**\n * Register the mime type and append_mime function with output_area\n */\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n /* Is output safe? */\n safe: true,\n /* Index of renderer in `output_area.display_order` */\n index: 0\n });\n }\n\n // register the mime type if in Jupyter Notebook environment and previously unregistered\n if (root.Jupyter !== undefined) {\n const events = require('base/js/events');\n const OutputArea = require('notebook/js/outputarea').OutputArea;\n\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n }\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"
    \\n\"+\n \"
  • re-rerun `output_notebook()` to attempt to load from CDN again, or
  • \\n\"+\n \"
  • use INLINE resources instead, as so:
  • \\n\"+\n \"
\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded(error = null) {\n const el = document.getElementById(\"dcea07c2-60f1-4255-aeb0-4b5123c89025\");\n if (el != null) {\n const html = (() => {\n if (typeof root.Bokeh === \"undefined\") {\n if (error == null) {\n return \"BokehJS is loading ...\";\n } else {\n return \"BokehJS failed to load.\";\n }\n } else {\n const prefix = `BokehJS ${root.Bokeh.version}`;\n if (error == null) {\n return `${prefix} successfully loaded.`;\n } else {\n return `${prefix} encountered errors while loading and may not function as expected.`;\n }\n }\n })();\n el.innerHTML = html;\n\n if (error != null) {\n const wrapper = document.createElement(\"div\");\n wrapper.style.overflow = \"auto\";\n wrapper.style.height = \"5em\";\n wrapper.style.resize = \"vertical\";\n const content = document.createElement(\"div\");\n content.style.fontFamily = \"monospace\";\n content.style.whiteSpace = \"pre-wrap\";\n content.style.backgroundColor = \"rgb(255, 221, 221)\";\n content.textContent = error.stack ?? error.toString();\n wrapper.append(content);\n el.append(wrapper);\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(() => display_loaded(error), 100);\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.7.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-3.7.3.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n try {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n\n } catch (error) {display_loaded(error);throw error;\n }if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(\"dcea07c2-60f1-4255-aeb0-4b5123c89025\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));", + "application/vnd.bokehjs_load.v0+json": "" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from dbase.database.db_utils import set_environment_context\n", + "set_environment_context(\"long_bbands\")\n", + "from algo.positions.loaders.position_vars import get_position_data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d51d47ce", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 11:46:17 [long_bbands] algo.positions.loaders.position_vars INFO: Loading position data for today: 2026-02-02\n", + "2026-02-02 11:46:17 [test] DataManager.py CRITICAL: Skipping MySQL query. This is not optimized and may lead to performance issues.\n", + "2026-02-02 11:46:17 [long_bbands] algo.positions.loaders.position_vars INFO: Loading position data (force=True, refresh=True, eod_block=False, is_today=False, date=2026-02-02)\n", + "[get_engine] Creating engine for DB: portfolio_config_long_bbands (base: portfolio_config), PID: 8931\n", + "2026-02-02 11:46:21 [long_bbands] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n", + "2026-02-02 11:46:21 [long_bbands] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n", + "2026-02-02 11:46:22 [long_bbands] algo.strategies._config_utils INFO: No configuration differences found for slug 'long_bbands'.\n", + "Fetching rates data from yfinance directly during market hours\n", + "Fetching rates data from yfinance directly during market hours\n", + "2026-02-02 11:46:33 [test] trade.asset.Stock ERROR: Error getting dividends history for AMD from yfinance\n", + "2026-02-02 11:46:33 [test] trade.asset.Stock ERROR: Probably due to no dividends history\n", + "Fetching rates data from yfinance directly during market hours\n", + "Fetching rates data from yfinance directly during market hours\n", + "2026-02-02 11:46:37 [test] trade.asset.Stock ERROR: Error setting close for BA: \n", + "[Error] -> Unauthorized FMP request -> Legacy Endpoint : Due to Legacy endpoints being no longer supported - This endpoint is only available for legacy users who have valid subscriptions prior August 31, 2025. Please visit our documentation page https://site.financialmodelingprep.com/developer/docs for our current APIs. \n", + "2026-02-02 11:46:37 [test] trade.asset.Stock ERROR: \n", + "set_variables raised an error\n", + "Traceback (most recent call last):\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/helpers/decorators.py\", line 394, in wrapper\n", + " return func(*args, **kwargs)\n", + " ^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/assets/Stock.py\", line 134, in set_variables\n", + " raise e ## Raise error so that decorator can catch it\n", + " ^^^^^^^\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/assets/Stock.py\", line 130, in set_variables\n", + " self.prev_close()\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/assets/Stock.py\", line 440, in prev_close\n", + " obb.equity.price.quote(symbol=self.ticker, provider=\"fmp\").to_dataframe()[\"prev_close\"].values[0]\n", + " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/openbb_core/app/static/utils/decorators.py\", line 93, in wrapper\n", + " raise UnauthorizedError(f\"\\n[Error] -> {e}\").with_traceback(\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/openbb_fmp/utils/helpers.py\", line 24, in response_callback\n", + " raise UnauthorizedError(f\"Unauthorized FMP request -> {error_message}\")\n", + "openbb_core.provider.utils.errors.UnauthorizedError: \n", + "[Error] -> Unauthorized FMP request -> Legacy Endpoint : Due to Legacy endpoints being no longer supported - This endpoint is only available for legacy users who have valid subscriptions prior August 31, 2025. Please visit our documentation page https://site.financialmodelingprep.com/developer/docs for our current APIs. \n", + "Fetching rates data from yfinance directly during market hours\n", + "Fetching rates data from yfinance directly during market hours\n", + "2026-02-02 11:46:50 [long_bbands] algo.positions.loaders.position_vars INFO: Using cached position data (last loaded at 2026-02-02 11:46:50.737208-05:00)\n" + ] + } + ], + "source": [ + "pos = get_position_data(force=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3d0f5b6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "OptionGreeks(bs_delta=0.0, bs_gamma=0.0, bs_theta=0.01783721107952374, bs_vega=0.0, bs_rho=-0.01517916777565631, bs_volga=nan, binomial_delta=0.0, binomial_gamma=0.0, binomial_theta=0.01783721107952374, binomial_vega=0.0, binomial_rho=-0.01517916777565631, dollar_bs_delta=np.float64(0.0), dollar_binomial_delta=np.float64(0.0))" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pos.strategies[\"long_bbands\"].positions[2].position_data.greeks" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/notebooks/theo_utils.ipynb b/trade/datamanager/notebooks/theo_utils.ipynb new file mode 100644 index 0000000..63a1b48 --- /dev/null +++ b/trade/datamanager/notebooks/theo_utils.ipynb @@ -0,0 +1,1580 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3c2333d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 00:07:25 trade.helpers.Logging INFO: Logging Root Directory: /Users/chiemelienwanisobi/cloned_repos/QuantTools/logs\n", + "2026-02-01 00:07:25 [test] trade.helpers.clear_cache INFO: No expired caches to delete on 2026-02-01.\n", + "2026-02-01 00:07:30 [test] dbase.DataAPI.ThetaData.proxy INFO: Refreshed proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-01 00:07:30 [test] dbase.DataAPI.ThetaData.proxy INFO: Using Proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-01 00:07:30 [test] dbase.DataAPI.ThetaData INFO: Using V2 of the ThetaData API\n", + "\n", + "\n", + "Scheduled Data Requests will be saved to: /Users/chiemelienwanisobi/cloned_repos/QuantTools/module_test/raw_code/DataManagers/scheduler/requests.jsonl\n", + "2026-02-01 00:07:34 [test] DataManager.py CRITICAL: Using ProcessSaveManager for saving data.\n", + "Fetching rates data from yfinance directly during market hours\n", + "YF.download() has changed argument auto_adjust default to True\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "import pandas as pd\n", + "import numpy as np\n", + "from typing import Optional, TypedDict, Literal, Dict, List, Any, Union, get_args, Iterable\n", + "from dataclasses import dataclass, field\n", + "from trade.datamanager.utils.model import (\n", + " LoadRequest,\n", + " _load_model_data_timeseries,\n", + " DivType,\n", + " VolatilityModel,\n", + " OptionPricingModel,\n", + ")\n", + "from trade.helpers.helper import time_distance_helper\n", + "from trade.datamanager.base import BaseDataManager\n", + "from trade.datamanager.config import OptionDataConfig\n", + "from trade.datamanager.result import _OptionModelResultsBase, Result\n", + "from trade.datamanager.result import (\n", + " VolatilityResult,\n", + " ForwardResult,\n", + " RatesResult,\n", + " OptionSpotResult,\n", + " SpotResult,\n", + " DividendsResult,\n", + ")\n", + "from trade.datamanager._enums import (\n", + " SeriesId,\n", + " GreekType,\n", + " OptionSpotEndpointSource,\n", + " Interval,\n", + " ArtifactType,\n", + " RealTimeFallbackOption,\n", + ")\n", + "from trade.datamanager.vars import LOADED_NAMES\n", + "from trade.datamanager.utils.model import ModelResultPack\n", + "from enum import Enum\n", + "from trade.datamanager.utils.cache import _check_cache_for_timeseries_data_structure\n", + "from trade.datamanager.utils.date import DATE_HINT\n", + "from trade.optionlib.greeks.numerical.binomial import binomial_tree_greeks\n", + "from trade.optionlib.greeks.numerical.black_scholes import vectorized_black_scholes_greeks\n", + "from trade.optionlib.assets.dividend import vectorized_discrete_pv, get_vectorized_continuous_dividends, vector_convert_to_time_frac\n", + "from trade.datamanager.utils.date import sync_date_index, is_available_on_date, to_datetime\n", + "from trade.helpers.helper import change_to_last_busday\n", + "from trade.datamanager import DividendDataManager\n", + "from datetime import datetime\n", + "from trade.helpers.Logging import setup_logger\n", + "from trade.datamanager.utils.vol_helpers import (\n", + " _prepare_vol_calculation_setup,\n", + " _handle_cache_for_vol,\n", + " _merge_and_cache_vol_result,\n", + ")\n", + "from trade.datamanager import (\n", + " DividendDataManager,\n", + " SpotDataManager,\n", + " OptionSpotDataManager,\n", + " VolDataManager,\n", + " RatesDataManager,\n", + " BaseDataManager,\n", + " ForwardDataManager,\n", + " GreekDataManager,\n", + " assert_synchronized_model,\n", + ")\n", + "from trade.optionlib.assets.dividend import get_vectorized_dividend_scehdule, get_div_histories\n", + "from trade.optionlib.pricing.binomial import vector_crr_binomial_pricing\n", + "from trade.optionlib.pricing.black_scholes import black_scholes_vectorized\n", + "from trade.datamanager.vars import load_name, clear_loaded_names\n", + "from trade.optionlib.assets.forward import vectorized_forward_continuous, vectorized_forward_discrete\n", + "logger = setup_logger(__name__)\n", + "CONFIG = OptionDataConfig()" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1afd5c6e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "TheoreticalPriceResult(symbol=None, strike=None, expiration=None, right=None, model_price=)" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "@dataclass\n", + "class TheoreticalPriceResult(_OptionModelResultsBase):\n", + " timeseries: Optional[pd.Series] = None\n", + "\n", + " def __repr__(self) -> str:\n", + " return super().__repr__() \n", + "\n", + "TheoreticalPriceResult()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "560b0a97", + "metadata": {}, + "outputs": [], + "source": [ + "from trade.datamanager._enums import ModelPrice\n", + "\n", + "\n", + "def _create_load_request(\n", + " ## Requied parameters to ensure correct data is loaded\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " symbol: str,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " dividend_type: DivType,\n", + " market_model: OptionPricingModel,\n", + " endpoint_source: OptionSpotEndpointSource,\n", + " model_price: ModelPrice,\n", + " is_scenario_load: bool = False,\n", + " *,\n", + " ## Optional pre-loaded data. If not provided, will be loaded.\n", + " s: Optional[SpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " f: Optional[ForwardResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " option_spot: Optional[OptionSpotResult] = None,\n", + " undo_adjust: bool = True,\n", + " \n", + ") -> LoadRequest:\n", + " \"\"\"Create a LoadRequest specifying which market data to load for greek calculation.\n", + "\n", + " Internal utility that determines which data sources need to be loaded based on:\n", + " 1. Which data is already provided (pre-loaded)\n", + " 2. Which pricing model is being used (BSM needs forwards, binomial needs spot)\n", + "\n", + " Args:\n", + " start_date: First valuation date (YYYY-MM-DD string or datetime).\n", + " end_date: Last valuation date (YYYY-MM-DD string or datetime).\n", + " expiration: Option expiration date (YYYY-MM-DD string or datetime).\n", + " strike: Strike price of the option.\n", + " right: Option type ('c' for call, 'p' for put).\n", + " dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS).\n", + " market_model: Pricing model (BSM or BINOMIAL).\n", + " endpoint_source: Option data source (ORATS, HIST, QUOTE).\n", + " model_price: Which price to use (CLOSE, OPEN, MIDPOINT).\n", + " s: Optional pre-loaded spot data. If None, will be loaded.\n", + " r: Optional pre-loaded rates data. If None, will be loaded.\n", + " f: Optional pre-loaded forward data. If None, will be loaded (BSM only).\n", + " d: Optional pre-loaded dividend data. If None, will be loaded.\n", + " vol: Optional pre-loaded volatility data. If None, will be loaded.\n", + " undo_adjust: If True, uses split-adjusted prices.\n", + "\n", + " Returns:\n", + " LoadRequest object with flags indicating which data sources to load.\n", + "\n", + " Examples:\n", + " >>> # Internal usage - creates request to load all data\n", + " >>> request = greek_mgr._create_load_request(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... expiration=\"2025-06-20\",\n", + " ... strike=150.0,\n", + " ... right=\"c\",\n", + " ... dividend_type=DivType.DISCRETE,\n", + " ... market_model=OptionPricingModel.BSM,\n", + " ... endpoint_source=OptionSpotEndpointSource.HIST,\n", + " ... model_price=ModelPrice.CLOSE\n", + " ... )\n", + " >>> # request.load_forward = True (BSM needs forwards)\n", + " >>> # request.load_spot = True (no spot provided)\n", + " >>> # request.load_vol = True (no vol provided)\n", + " \"\"\"\n", + " if is_scenario_load:\n", + " ## For scenario loads, always load all data to ensure completeness.\n", + " load_spot = s is None\n", + " load_vol = vol is None\n", + " load_dividend = d is None\n", + " load_rates = r is None\n", + " option_spot = option_spot is None\n", + " load_forward = False ## Not needed for scenario load\n", + " else:\n", + " \n", + " ## For regular loads, determine based on provided data and model needs.\n", + " load_spot = (s is None) and (market_model == OptionPricingModel.BINOMIAL)\n", + " load_vol = (vol is None)\n", + " load_dividend = (d is None)\n", + " load_rates = (r is None)\n", + " option_spot = False ## Not needed for greek calculation\n", + " load_forward = (market_model == OptionPricingModel.BSM) and (f is None)\n", + "\n", + " req = LoadRequest(\n", + " symbol=symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " endpoint_source=endpoint_source,\n", + " vol_model=VolatilityModel.MARKET,\n", + " model_price=model_price,\n", + " market_model=market_model,\n", + " \n", + " ## Load spot only if missing.\n", + " load_spot=load_spot,\n", + " \n", + " ## Load forward only if missing and using BSM model. Binomial uses spot price.\n", + " load_forward=load_forward,\n", + " load_vol=load_vol,\n", + " load_dividend=load_dividend,\n", + " load_rates=load_rates,\n", + " \n", + " ## Not needed for greek calculation\n", + " load_option_spot=option_spot,\n", + " undo_adjust=undo_adjust,\n", + " )\n", + " return req" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "138762ab", + "metadata": {}, + "outputs": [], + "source": [ + "def get_option_theoretical_price(\n", + " symbol: str,\n", + " start_date: DATE_HINT,\n", + " end_date: DATE_HINT,\n", + " strike: float,\n", + " expiration: DATE_HINT,\n", + " right: Literal['c', 'p'],\n", + " *,\n", + " market_model: Optional[OptionPricingModel] = None,\n", + " endpoint_source: OptionSpotEndpointSource = None,\n", + " dividend_type: Optional[DivType] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " model_price: Optional[ModelPrice] = None,\n", + " spot: Optional[SpotResult] = None,\n", + " f: Optional[ForwardResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + ") -> TheoreticalPriceResult:\n", + " \"\"\"Calculate theoretical option prices over a date range using specified pricing model.\n", + "\n", + " Args:\n", + " symbol (str): Underlying asset symbol.\n", + " start_date (DATE_HINT): Start date for the calculation.\n", + " end_date (DATE_HINT): End date for the calculation.\n", + " strike (float): Option strike price.\n", + " expiration (DATE_HINT): Option expiration date.\n", + " right (Literal['c', 'p']): 'c' for call, 'p' for put option.\n", + " market_model (Optional[OptionPricingModel], optional): Pricing model to use. Defaults to None.\n", + " endpoint_source (OptionSpotEndpointSource, optional): Source for option spot data. Defaults to None.\n", + " dividend_type (Optional[DivType], optional): Type of dividend adjustment. Defaults to None.\n", + " vol (Optional[VolatilityModel], optional): Volatility model to use. Defaults to None.\n", + " spot (Optional[float], optional): Spot price of the underlying asset. Defaults to None.\n", + " f (Optional[ForwardResult], optional): Forward price of the underlying asset. Defaults to None.\n", + " r (Optional[RatesResult], optional): Risk-free interest rate. Defaults to None.\n", + " option_spot (Optional[OptionSpotResult], optional): Spot price of the option. Defaults to None.\n", + " n_steps (Optional[int], optional): Number of steps for binomial model. Defaults to None.\n", + " Returns:\n", + " TheoreticalPriceResult: Result object containing theoretical prices time series.\n", + " \"\"\"\n", + " market_model = market_model or CONFIG.option_model\n", + " endpoint_source = endpoint_source or CONFIG.option_spot_endpoint_source\n", + " dividend_type = dividend_type or CONFIG.dividend_type\n", + " vol_model = CONFIG.volatility_model\n", + " model_price = model_price or CONFIG.model_price\n", + " n_steps = n_steps or CONFIG.n_steps\n", + " result = TheoreticalPriceResult()\n", + " result.dividend_type = dividend_type\n", + " result.market_model = market_model\n", + " result.model_price = model_price\n", + " result.vol_model = vol_model\n", + " result.endpoint_source = endpoint_source\n", + " result.expiration = to_datetime(expiration)\n", + " result.right = right\n", + " result.strike = strike\n", + " result.symbol = symbol\n", + " result.rt = False\n", + " result.undo_adjust = undo_adjust\n", + "\n", + " # Create load request to determine which data to load\n", + " load_request = _create_load_request(\n", + " symbol=symbol,\n", + " start_date=start_date,\n", + " end_date=end_date,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " market_model=market_model,\n", + " endpoint_source=endpoint_source,\n", + " model_price=model_price,\n", + " s=spot,\n", + " f=f,\n", + " r=r,\n", + " vol=vol,\n", + " is_scenario_load=False,\n", + " )\n", + "\n", + " # Load required market data\n", + " packet = _load_model_data_timeseries(load_request)\n", + "\n", + " # Extract time series data, using provided data if available\n", + " s, r, vol, d, f = (\n", + " packet.spot.timeseries if not packet.spot.is_empty() else spot.timeseries if spot is not None else pd.Series(dtype=float),\n", + " packet.rates.timeseries if not packet.rates.is_empty() else r.timeseries if r is not None else pd.Series(dtype=float),\n", + " packet.vol.timeseries if not packet.vol.is_empty() else vol.timeseries if vol is not None else pd.Series(dtype=float),\n", + " packet.dividend.timeseries if not packet.dividend.is_empty() else d.timeseries if d is not None else pd.Series(dtype=float),\n", + " packet.forward.timeseries if not packet.forward.is_empty() else f.timeseries if f is not None else pd.Series(dtype=float),\n", + " )\n", + " \n", + "\n", + " # Use loaded data to calculate theoretical prices\n", + " if market_model == OptionPricingModel.BINOMIAL:\n", + " s, vol, r, d = sync_date_index(s, vol, r, d)\n", + " t = time_distance_helper(start=s.index, end = [expiration] * len(s))\n", + " if dividend_type == DivType.DISCRETE:\n", + " discrete = vector_convert_to_time_frac(\n", + " schedules=d.values,\n", + " valuation_dates=d.index,\n", + " end_dates=[to_datetime(expiration)] * len(s),\n", + " )\n", + " dividend_yield = [0.0] * len(s)\n", + " else:\n", + " discrete = [()] * len(s)\n", + " dividend_yield = d.values\n", + "\n", + " prices = vector_crr_binomial_pricing(\n", + " K = [strike] * len(s),\n", + " T = t,\n", + " sigma = vol.values,\n", + " r = r.values,\n", + " N = [n_steps] * len(s),\n", + " S0 = s.values,\n", + " right = [right] * len(s),\n", + " american = [True] * len(s),\n", + " dividend_yield=dividend_yield,\n", + " dividends=discrete,\n", + " dividend_type=[dividend_type.value] * len(s),\n", + " )\n", + " result.timeseries = pd.Series(data=prices, index=s.index, name='theoretical_price', dtype=float)\n", + " return result\n", + " \n", + " elif market_model == OptionPricingModel.BSM:\n", + " f, vol, r, d = packet.forward.timeseries, packet.vol.timeseries, packet.rates.timeseries, packet.dividend.timeseries\n", + " f, vol, r, d = sync_date_index(f, vol, r, d)\n", + " t = time_distance_helper(start=f.index, end = [expiration] * len(f))\n", + " prices = black_scholes_vectorized(\n", + " F = f.values,\n", + " K = [strike] * len(f),\n", + " T = t,\n", + " r = r.values,\n", + " sigma = vol.values,\n", + " option_type = [right] * len(f),\n", + " )\n", + " result.timeseries = pd.Series(data=prices, index=f.index, name='theoretical_price', dtype=float)\n", + " return result\n", + "\n", + "\n", + " \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "058d3286", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "DEFAULT_SCENARIOS = [0.9, 0.95, 1.0, 1.05, 1.1]\n", + "DEFAULT_VOL_SCENARIOS = [-0.2, -0.1, 0.0, 0.1, 0.2]\n", + "@dataclass\n", + "class ScenariosResult(_OptionModelResultsBase):\n", + " grid: Optional[pd.DataFrame] = None\n", + " spot_scenarios: List[float] = field(default_factory=lambda: DEFAULT_SCENARIOS)\n", + " vol_scenarios: List[float] = field(default_factory=lambda: DEFAULT_VOL_SCENARIOS)\n", + " def __repr__(self) -> str:\n", + " return super().__repr__()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a5034cdb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'model_input_keys': None,\n", + " 'rt': False,\n", + " 'symbol': None,\n", + " 'strike': None,\n", + " 'expiration': None,\n", + " 'right': None,\n", + " 'model_price': ,\n", + " 'endpoint_source': None,\n", + " 'market_model': None,\n", + " 'vol_model': None,\n", + " 'dividend_type': None,\n", + " 'undo_adjust': None}" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_OptionModelResultsBase().__dict__" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0aae8990", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "y = 0.005\n", + "new_y = 0.005555555555555556\n", + "Shock applied on original y: 0.005555555555555556\n" + ] + } + ], + "source": [ + "s = 100\n", + "div = 0.5\n", + "y = div/s\n", + "\n", + "print(f\"y = {y}\")\n", + "\n", + "shock = 0.9\n", + "new_s = s * shock\n", + "new_y = div/new_s\n", + "\n", + "print(f\"new_y = {new_y}\")\n", + "print(f\"Shock applied on original y: {y / shock}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a013ce4c", + "metadata": {}, + "outputs": [], + "source": [ + "def _adjust_div_yield_for_spot_shock(\n", + " shock: float,\n", + " div: float,\n", + ") -> float:\n", + " \"\"\"Adjust dividend yield based on spot price shock for continuous dividends.\"\"\"\n", + " adjusted_div = div / shock\n", + " return adjusted_div" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5cc16dd9", + "metadata": {}, + "outputs": [], + "source": [ + "from itertools import product\n", + "def _calculate_binomial_scenarios(\n", + " base_prices: pd.Series,\n", + " s: pd.Series,\n", + " strike: float,\n", + " expiration: DATE_HINT,\n", + " right: Literal['c', 'p'],\n", + " vol: pd.Series,\n", + " r: pd.Series,\n", + " dividend_type: DivType,\n", + " dividends: pd.Series,\n", + " spot_scenarios: List[float] = None,\n", + " vol_scenarios: List[float] = None,\n", + " return_pnl: bool = False,\n", + " return_pnl_in_pct: bool = False,\n", + " n_steps: int = None,\n", + " prettify_columns: bool = False,\n", + ") -> pd.DataFrame:\n", + " \"\"\"Calculate spot price scenarios using binomial model.\n", + "\n", + " Args:\n", + " base_prices (pd.Series): Base theoretical prices time series.\n", + " s (pd.Series): Spot prices time series.\n", + " strike (float): Option strike price.\n", + " expiration (DATE_HINT): Option expiration date.\n", + " right (Literal['c', 'p']): 'c' for call, 'p' for put option.\n", + " vol (pd.Series): Volatility time series.\n", + " r (pd.Series): Risk-free interest rate time series.\n", + " dividend_type (DivType): Type of dividend adjustment.\n", + " spot_scenarios (List[float]): List of spot price multipliers to generate scenarios. Spot scenarios are multiplied to the spot price to generate different spot price levels.\n", + " vol_scenarios: List[float]: List of volatility multipliers to generate scenarios. Vol scenarios are added to the volatility to generate different volatility levels.\n", + " n_steps (int): Number of steps for binomial model.\n", + "\n", + " Returns:\n", + " pd.DataFrame: DataFrame containing spot price scenarios.\n", + " \"\"\"\n", + " assert any([spot_scenarios, vol_scenarios]), \"At least one of spot_scenarios or vol_scenarios must be provided.\"\n", + " assert len(vol.index) == 1, \"Spot scenarios calculation only supports single-date series.\"\n", + " \n", + " ## Default scenarios\n", + " if spot_scenarios is None:\n", + " spot_scenarios = [1.0]\n", + " if vol_scenarios is None:\n", + " vol_scenarios = [0.0]\n", + " \n", + " ## Sync all data to same index\n", + " s, vol, r, dividends, base_prices = sync_date_index(s, vol, r, dividends, base_prices)\n", + " scenario_prices: Dict[str, pd.Series] = {}\n", + "\n", + " ## Define pricing function for reuse\n", + " def price_func(scenario_spot: pd.Series, \n", + " scenario_vol: pd.Series,\n", + " expiration: DATE_HINT,\n", + " right: Literal['c', 'p'],\n", + " strike: float,\n", + " dividend_type: DivType,\n", + " dividends: pd.Series,\n", + " n_steps: int,\n", + " r: pd.Series,\n", + " ) -> pd.Series:\n", + " t = time_distance_helper(start=scenario_spot.index, end = [expiration] * len(scenario_spot))\n", + " if dividend_type == DivType.DISCRETE:\n", + " discrete = vector_convert_to_time_frac(\n", + " schedules=dividends.values,\n", + " valuation_dates=scenario_spot.index,\n", + " end_dates=[to_datetime(expiration)] * len(scenario_spot),\n", + " )\n", + " dividend_yield = [0.0] * len(scenario_spot)\n", + " else:\n", + " discrete = [()] * len(scenario_spot)\n", + " dividend_yield = dividends.values\n", + "\n", + " prices = vector_crr_binomial_pricing(\n", + " K = [strike] * len(scenario_spot),\n", + " T = t,\n", + " sigma = scenario_vol.values,\n", + " r = r.values,\n", + " N = [n_steps] * len(scenario_spot),\n", + " S0 = scenario_spot.values,\n", + " right = [right] * len(scenario_spot),\n", + " american = [True] * len(scenario_spot),\n", + " dividend_yield=dividend_yield,\n", + " dividends=discrete,\n", + " dividend_type=[dividend_type.value] * len(scenario_spot),\n", + " )\n", + " return pd.Series(data=prices, index=scenario_spot.index, name='theoretical_price', dtype=float)\n", + " \n", + "\n", + " ## Calculate prices for each scenario\n", + " scenarios = list(product(spot_scenarios, vol_scenarios))\n", + " for spot_mult, vol_add in scenarios:\n", + " scenario_spot = s * spot_mult\n", + " scenario_vol = vol + vol_add\n", + " if dividend_type == DivType.CONTINUOUS:\n", + " \n", + " adjusted_dividends = _adjust_div_yield_for_spot_shock(spot_mult, dividends)\n", + " else:\n", + " adjusted_dividends = dividends\n", + "\n", + " prices = price_func(scenario_spot, scenario_vol, expiration, right, strike, dividend_type, adjusted_dividends, n_steps, r)\n", + " prices = prices[0]\n", + " if return_pnl:\n", + " prices = prices - base_prices[0]\n", + " if return_pnl_in_pct:\n", + " prices = prices / base_prices[0]\n", + " scenario_prices.setdefault(spot_mult, []).append(prices)\n", + "\n", + "\n", + "\n", + " df = pd.DataFrame(scenario_prices, index=vol_scenarios)\n", + " if prettify_columns:\n", + " df.columns = [f\"Spot x{col:.2f}\" for col in df.columns]\n", + " df.index = [f\"Vol {'+' if idx > 0 else ''}{idx:.2%}\" for idx in df.index]\n", + " return df\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6911dc23", + "metadata": {}, + "outputs": [], + "source": [ + "def _calculate_bsm_scenarios(\n", + " base_prices: pd.Series,\n", + " s: pd.Series,\n", + " strike: float,\n", + " expiration: DATE_HINT,\n", + " right: Literal['c', 'p'],\n", + " vol: pd.Series,\n", + " r: pd.Series,\n", + " dividend_type: DivType,\n", + " pv_divs: pd.Series = None,\n", + " q_factor: pd.Series = None,\n", + " spot_scenarios: List[float] = None,\n", + " vol_scenarios: List[float] = None,\n", + " return_pnl: bool = False,\n", + " return_pnl_in_pct: bool = False,\n", + " prettify_columns: bool = False,\n", + ") -> pd.DataFrame:\n", + " \"\"\"Calculate spot price scenarios using Black-Scholes-Merton model.\n", + "\n", + " Args:\n", + " base_prices (pd.Series): Base theoretical prices time series.\n", + " s (pd.Series): Spot prices time series.\n", + " strike (float): Option strike price.\n", + " expiration (DATE_HINT): Option expiration date.\n", + " right (Literal['c', 'p']): 'c' for call, 'p' for put option.\n", + " vol (pd.Series): Volatility time series.\n", + " r (pd.Series): Risk-free interest rate time series.\n", + " dividend_type (DivType): Type of dividend adjustment.\n", + " spot_scenarios (List[float]): List of spot price multipliers to generate scenarios. Spot scenarios are multiplied to the spot price to generate different spot price levels.\n", + " vol_scenarios: List[float]: List of volatility multipliers to generate scenarios. Vol scenarios are added to the volatility to generate different volatility levels.\n", + "\n", + " Returns:\n", + " pd.DataFrame: DataFrame containing spot price scenarios.\n", + " \"\"\"\n", + " assert any([spot_scenarios, vol_scenarios]), \"At least one of spot_scenarios or vol_scenarios must be provided.\"\n", + " assert len(vol.index) == 1, \"Spot scenarios calculation only supports single-date series.\"\n", + " \n", + " ## Default scenarios\n", + " if spot_scenarios is None:\n", + " spot_scenarios = [1.0]\n", + " if vol_scenarios is None:\n", + " vol_scenarios = [0.0]\n", + "\n", + " if dividend_type == DivType.CONTINUOUS:\n", + " assert q_factor is not None, \"For continuous dividends, q_factor must be provided.\"\n", + " dividends = q_factor\n", + " else:\n", + " assert pv_divs is not None, \"For discrete dividends, pv_divs must be provided.\"\n", + " dividends = pv_divs\n", + " \n", + " ## Sync all data to same index\n", + " s, vol, r, dividends, base_prices = sync_date_index(s, vol, r, dividends, base_prices)\n", + " scenario_prices: Dict[str, pd.Series] = {}\n", + "\n", + " ## Define pricing function for reuse\n", + " def price_func(scenario_spot: pd.Series, \n", + " scenario_vol: pd.Series,\n", + " expiration: DATE_HINT,\n", + " right: Literal['c', 'p'],\n", + " strike: float,\n", + " ) -> pd.Series:\n", + " t = time_distance_helper(start=scenario_spot.index, end = [expiration] * len(scenario_spot))\n", + " if dividend_type == DivType.CONTINUOUS:\n", + " F = vectorized_forward_continuous(\n", + " S = scenario_spot.values,\n", + " r = r.values,\n", + " q_factor = dividends.values,\n", + " T = t,\n", + " )\n", + " else:\n", + " F = vectorized_forward_discrete(\n", + " S = scenario_spot.values,\n", + " r = r.values,\n", + " pv_divs = dividends.values,\n", + " T = t,\n", + " )\n", + " prices = black_scholes_vectorized(\n", + " F = F,\n", + " K = [strike] * len(scenario_spot),\n", + " T = t,\n", + " r = r.values,\n", + " sigma = scenario_vol.values,\n", + " option_type = [right] * len(scenario_spot),\n", + " )\n", + " return pd.Series(data=prices, index=scenario_spot.index, name='theoretical_price', dtype=float)\n", + " \n", + " ## Calculate prices for each scenarios\n", + " scenarios = list(product(spot_scenarios, vol_scenarios))\n", + " for spot_mult, vol_add in scenarios:\n", + " scenario_spot = s * spot_mult\n", + " scenario_vol = vol + vol_add\n", + "\n", + " prices = price_func(scenario_spot, scenario_vol, expiration, right, strike)\n", + " prices = prices[0]\n", + " if return_pnl:\n", + " prices = prices - base_prices[0]\n", + " if return_pnl_in_pct:\n", + " prices = prices / base_prices[0]\n", + " scenario_prices.setdefault(spot_mult, []).append(prices)\n", + "\n", + " df = pd.DataFrame(scenario_prices, index=vol_scenarios)\n", + " if prettify_columns:\n", + " df.columns = [f\"Spot x{col:.2f}\" for col in df.columns]\n", + " df.index = [f\"Vol {'+' if idx > 0 else ''}{idx:.2%}\" for idx in df.index]\n", + " return df" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "1d4d451f", + "metadata": {}, + "outputs": [], + "source": [ + "def calculate_scenarios(\n", + " symbol: str,\n", + " as_of: DATE_HINT,\n", + " strike: float,\n", + " expiration: DATE_HINT,\n", + " right: Literal[\"c\", \"p\"],\n", + " spot_scenarios: Optional[List[float]] = None,\n", + " vol_scenarios: Optional[List[float]] = None,\n", + " *,\n", + " market_model: Optional[OptionPricingModel] = None,\n", + " endpoint_source: OptionSpotEndpointSource = None,\n", + " dividend_type: Optional[DivType] = None,\n", + " vol: Optional[VolatilityResult] = None,\n", + " model_price: Optional[ModelPrice] = None,\n", + " spot: Optional[SpotResult] = None,\n", + " option_spot: Optional[OptionSpotResult] = None,\n", + " r: Optional[RatesResult] = None,\n", + " d: Optional[DividendsResult] = None,\n", + " undo_adjust: bool = True,\n", + " n_steps: Optional[int] = None,\n", + " prettify_columns: bool = False,\n", + " return_pnl: bool = False,\n", + " return_pnl_in_pct: bool = False,\n", + ") -> ScenariosResult:\n", + " \"\"\"Calculate spot price scenarios for an option over a date range using specified pricing model.\n", + "\n", + " Args:\n", + " symbol (str): Underlying asset symbol.\n", + " as_of (DATE_HINT): Date for the calculation.\n", + " strike (float): Option strike price.\n", + " expiration (DATE_HINT): Option expiration date.\n", + " right (Literal['c', 'p']): 'c' for call, 'p' for put option.\n", + " market_model (Optional[OptionPricingModel], optional): Pricing model to use. Defaults to None.\n", + " endpoint_source (OptionSpotEndpointSource, optional): Source for option spot data. Defaults to None.\n", + " dividend_type (Optional[DivType], optional): Type of dividend adjustment. Defaults to None.\n", + " vol (Optional[VolatilityResult], optional): Volatility model to use. Defaults to None.\n", + " spot (Optional[float], optional): Spot price of the underlying asset. Defaults to None.\n", + " f (Optional[float], optional): Forward price of the underlying asset. Defaults to None.\n", + " r (Optional[float], optional): Risk-free interest rate. Defaults to None.\n", + " n_steps (Optional[int], optional): Number of steps for binomial model. Defaults to None.\n", + " default_scenarios (Optional[List[float]], optional): List of spot price multipliers to generate scenarios. Defaults to None.\n", + "\n", + " Returns:\n", + " SpotScenariosResult: Result object containing spot price scenarios time series.\n", + " \"\"\"\n", + " market_model = market_model or CONFIG.option_model\n", + " endpoint_source = endpoint_source or CONFIG.option_spot_endpoint_source\n", + " dividend_type = dividend_type or CONFIG.dividend_type\n", + " vol_model = vol or CONFIG.volatility_model\n", + " model_price = model_price or CONFIG.model_price\n", + " n_steps = n_steps or CONFIG.n_steps\n", + " spot_scenarios = spot_scenarios or DEFAULT_SCENARIOS\n", + " vol_scenarios = vol_scenarios or DEFAULT_VOL_SCENARIOS\n", + " result = ScenariosResult()\n", + " \n", + " result.dividend_type = dividend_type\n", + " result.market_model = market_model\n", + " result.model_price = model_price\n", + " result.vol_model = vol_model\n", + " result.endpoint_source = endpoint_source\n", + " result.expiration = to_datetime(expiration)\n", + " result.right = right\n", + " result.strike = strike\n", + " result.symbol = symbol\n", + " result.rt = False\n", + " result.undo_adjust = undo_adjust\n", + " result.spot_scenarios = spot_scenarios\n", + " result.vol_scenarios = vol_scenarios\n", + "\n", + " # Create load request to determine which data to load\n", + " load_request = _create_load_request(\n", + " symbol=symbol,\n", + " start_date=as_of,\n", + " end_date=as_of,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " dividend_type=dividend_type,\n", + " market_model=market_model,\n", + " endpoint_source=endpoint_source,\n", + " model_price=model_price,\n", + " s=spot,\n", + " r=r,\n", + " d=d,\n", + " vol=vol,\n", + " option_spot=option_spot,\n", + " is_scenario_load=True,\n", + " )\n", + " \n", + " # Load required market data\n", + " packet = _load_model_data_timeseries(load_request)\n", + " \n", + " s, r, vol, base_prices, d = (\n", + " packet.spot.timeseries if not packet.spot.is_empty() else spot.timeseries,\n", + " packet.rates.timeseries if not packet.rates.is_empty() else r.timeseries,\n", + " packet.vol.timeseries if not packet.vol.is_empty() else vol.timeseries,\n", + " packet.option_spot.price if not packet.option_spot.is_empty() else option_spot.price,\n", + " packet.dividend.timeseries if not packet.dividend.is_empty() else d.timeseries,\n", + " )\n", + " # Use loaded data to calculate theoretical prices\n", + " if market_model == OptionPricingModel.BINOMIAL:\n", + " s, vol, r, d, base_prices = sync_date_index(s, vol, r, d, base_prices)\n", + " df = _calculate_binomial_scenarios(\n", + " base_prices=base_prices,\n", + " s=s,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " vol=vol,\n", + " r=r,\n", + " dividend_type=dividend_type,\n", + " dividends=d,\n", + " spot_scenarios=spot_scenarios,\n", + " vol_scenarios=vol_scenarios,\n", + " n_steps=n_steps,\n", + " prettify_columns=prettify_columns,\n", + " return_pnl=return_pnl,\n", + " return_pnl_in_pct=return_pnl_in_pct,\n", + " )\n", + " result.grid = df\n", + " return result\n", + " \n", + " ## BSM model\n", + " elif market_model == OptionPricingModel.BSM:\n", + " s, vol, r, d, base_prices = sync_date_index(s, vol, r, d, base_prices)\n", + " if dividend_type == DivType.DISCRETE:\n", + " pv_divs = vectorized_discrete_pv(\n", + " schedules=d.values,\n", + " _valuation_dates=s.index,\n", + " _end_dates=[to_datetime(expiration)] * len(s),\n", + " r=r.values,\n", + " )\n", + " pv_divs = pd.Series(data=pv_divs, index=s.index, name='pv_dividends', dtype=float)\n", + " q_factor = None\n", + " else:\n", + " pv_divs = None\n", + " q_factor = get_vectorized_continuous_dividends(\n", + " div_rates=d.values,\n", + " _valuation_dates=s.index,\n", + " _end_dates=[to_datetime(expiration)] * len(s),\n", + " )\n", + " q_factor = pd.Series(data=q_factor, index=s.index, name='q_factor', dtype=float)\n", + " df = _calculate_bsm_scenarios(\n", + " base_prices=base_prices,\n", + " s=s,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right,\n", + " vol=vol,\n", + " r=r,\n", + " dividend_type=dividend_type,\n", + " pv_divs=pv_divs,\n", + " q_factor=q_factor,\n", + " spot_scenarios=spot_scenarios,\n", + " vol_scenarios=vol_scenarios,\n", + " prettify_columns=prettify_columns,\n", + " return_pnl=return_pnl,\n", + " return_pnl_in_pct=return_pnl_in_pct,\n", + " )\n", + " result.grid = df\n", + " return result" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "9f1beb1b", + "metadata": {}, + "outputs": [], + "source": [ + "pd.options.display.float_format = '{:.2f}'.format" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "284455c0", + "metadata": {}, + "outputs": [], + "source": [ + "symbol = \"SBUX\"\n", + "expiration = \"2026-09-18\"\n", + "right = \"C\"\n", + "strike = 100.0\n", + "ts_start = \"2025-01-01\"\n", + "ts_end = \"2026-01-28\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "c3798e31", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-01 00:54:20 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 00:54:20 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-02-01 00:54:20 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-06-20 00:00:00 to 2025-06-20 00:00:00 with maturity 2026-09-18 00:00:00\n", + "2026-02-01 00:54:20 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 00:54:20 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-02-01 00:54:20 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 00:54:20 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-06-20 to 2025-06-20...\n", + "2026-02-01 00:54:20 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-02-01 00:54:20 [test] EventDriven.riskmanager.market_data WARNING: End date 2025-06-20 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-02-01 00:54:20 [test] EventDriven.riskmanager.market_data INFO: Sanitizing today's data from all stored timeseries data...\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-06-20 00:00:00 to 2025-06-20 00:00:00...\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Using cached date range for 2025-06-20 00:00:00 - 2025-06-20 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-06-20 00:00:00 to 2025-06-20 00:00:00...\n", + "2026-02-01 00:54:20 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:100\n", + "2026-02-01 00:54:20 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-02-01 00:54:20 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Using cached date range for 2025-06-20 00:00:00 - 2025-06-20 00:00:00 and option tick SBUX20260918C100\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-06-20 00:00:00 to 2025-06-20 00:00:00...\n", + "2026-02-01 00:54:20 [test] trade.datamanager.utils INFO: Cache hit for vol timeseries key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:discrete|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:100|volatility_model:market\n" + ] + } + ], + "source": [ + "res = calculate_scenarios(\n", + " symbol=symbol,\n", + " as_of=\"2025-06-20\",\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right.lower(),\n", + " market_model=OptionPricingModel.BINOMIAL,\n", + " dividend_type=DivType.DISCRETE,\n", + " n_steps=50,\n", + " prettify_columns=True,\n", + " return_pnl=True,\n", + " return_pnl_in_pct=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "e0162c30", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Spot x0.90Spot x0.95Spot x1.00Spot x1.05Spot x1.10
Vol -20.00%-10.34-9.52-8.14-6.13-3.51
Vol -10.00%-7.64-6.04-4.10-1.870.79
Vol 0.00%-4.23-2.21-0.092.465.10
Vol +10.00%-0.531.644.036.719.39
Vol +20.00%3.155.418.1410.8813.62
\n", + "
" + ], + "text/plain": [ + " Spot x0.90 Spot x0.95 Spot x1.00 Spot x1.05 Spot x1.10\n", + "Vol -20.00% -10.34 -9.52 -8.14 -6.13 -3.51\n", + "Vol -10.00% -7.64 -6.04 -4.10 -1.87 0.79\n", + "Vol 0.00% -4.23 -2.21 -0.09 2.46 5.10\n", + "Vol +10.00% -0.53 1.64 4.03 6.71 9.39\n", + "Vol +20.00% 3.15 5.41 8.14 10.88 13.62" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "res.grid" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31940106", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-31 23:27:30 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-01-31 23:27:30 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-01-31 23:27:30 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-01-31 23:27:30 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n" + ] + } + ], + "source": [ + "dm = OptionSpotDataManager(symbol=symbol)\n", + "market = dm.get_option_spot_timeseries(\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right.lower(),\n", + " endpoint_source=OptionSpotEndpointSource.QUOTE,\n", + " model_price=ModelPrice.MIDPOINT,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffb247d6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-31 23:30:38 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-01-31 23:30:38 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-01-31 23:30:38 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-01-01 00:00:00 to 2026-01-28 00:00:00 with maturity 2026-09-18 00:00:00\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-01-31 23:30:38 [test] trade.datamanager.dividend INFO: No cache found for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1. Building from scratch.\n", + "2026-01-31 23:30:38 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-01-31 23:30:38 [test] trade.datamanager.dividend INFO: Cache hit for key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE\n", + "2026-01-31 23:30:38 [test] trade.datamanager.dividend INFO: Cache partially covers requested date range. Key: symbol:SBUX|interval:na|artifact_type:divs|series_id:hist|current_state:SCHEDULE|lookback_years:1|method:CONSTANT|vendor:YFINANCE. Fetching missing data.\n", + "2026-01-31 23:30:42 [test] trade.optionlib.assets.dividend INFO: Using dual projection method for ticker SBUX\n", + "2026-01-31 23:30:42 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size before adjustment: 15, for original valuation: 7. Size from historical divs: 12\n", + "2026-01-31 23:30:42 [test] trade.optionlib.assets.dividend INFO: Expected Dividend Size to be projected: 3\n", + "2026-01-31 23:30:42 [test] trade.optionlib.assets.dividend INFO: Projected Dividend List: [0.62, 0.62, 0.62]\n", + "2026-01-31 23:30:42 [test] trade.optionlib.assets.dividend INFO: Combined Dividend List: [0.53, 0.53, 0.53, 0.57, 0.57, 0.57, 0.57, 0.61, 0.61, 0.61, 0.61, 0.62, 0.62, 0.62, 0.62]\n", + "2026-01-31 23:30:42 [test] trade.optionlib.assets.dividend INFO: Combined Date List: [datetime.date(2023, 2, 9), datetime.date(2023, 5, 11), datetime.date(2023, 8, 10), datetime.date(2023, 11, 9), datetime.date(2024, 2, 8), datetime.date(2024, 5, 16), datetime.date(2024, 8, 16), datetime.date(2024, 11, 15), datetime.date(2025, 2, 14), datetime.date(2025, 5, 16), datetime.date(2025, 8, 15), datetime.date(2025, 11, 14), datetime.date(2026, 2, 14), datetime.date(2026, 5, 14), datetime.date(2026, 8, 14)]\n", + "2026-01-31 23:30:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-01-31 23:30:42 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1 to avoid saving partial day data.\n", + "2026-01-31 23:30:42 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-01-31 23:30:42 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-01-31 23:30:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-01-28...\n", + "2026-01-31 23:30:42 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-01-31 23:30:42 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-28 00:00:00 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-01-31 23:30:43 [test] EventDriven.riskmanager.market_data INFO: Current time is after 6 PM NY time. Skipping sanitization of today's data.\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-01-31 23:30:43 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:SBUX|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:QUOTE|expiration:20260918T000000|right:C|strike:100\n", + "2026-01-31 23:30:43 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.DISCRETE\n", + "2026-01-31 23:30:43 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Using cached date range for 2025-01-01 00:00:00 - 2026-01-28 00:00:00 and option tick SBUX20260918C100\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: No cache found for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market. Fetching from source.\n", + "2026-01-31 23:30:43 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-01-31 23:30:43 [test] trade.datamanager.vars INFO: Timeseries for SBUX already loaded.\n", + "2026-01-31 23:30:43 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.DISCRETE\n", + "2026-01-31 23:30:43 [test] trade.datamanager.dividend INFO: Fetching discrete dividend schedule timeseries for SBUX from 2025-05-23 to 2026-01-28 with maturity 2026-09-18\n", + "2026-01-31 23:30:43 [test] trade.datamanager.dividend INFO: Cache hit for discrete schedule timeseries key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-01-31 23:30:43 [test] trade.datamanager.dividend INFO: Cache fully covers requested date range for timeseries. Key: symbol:SBUX|interval:eod|artifact_type:divs|series_id:hist|current_state:SCHEDULE_TIMESERIES|lookback_years:1|maturity:2026-09-18|method:CONSTANT|undo_adjust:1\n", + "2026-01-31 23:30:43 [test] EventDriven.riskmanager.market_data WARNING: End date 2026-01-31 22:27:21.452185 is in the past or current time is after market close. Preload check will be skipped if specified.\n", + "2026-01-31 23:30:43 [test] EventDriven.riskmanager.market_data INFO: Current time is after 6 PM NY time. Skipping sanitization of today's data.\n", + "2026-01-31 23:30:43 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-01-31 23:30:43 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:forward|series_id:hist|dividend_type:DISCRETE|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-01-31 23:30:43 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 00:00:00 to 2026-01-28 00:00:00...\n", + "2026-01-31 23:30:44 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:SBUX|interval:eod|artifact_type:iv|series_id:hist|dividend_type:discrete|endpoint_source:quote|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Black-Scholes|right:C|strike:100|volatility_model:market to avoid saving partial day data.\n", + "2026-01-31 23:30:44 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-05-23 to 2026-01-28...\n" + ] + } + ], + "source": [ + "p = get_option_theoretical_price(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " strike=strike,\n", + " expiration=expiration,\n", + " right=right.lower(),\n", + " market_model=OptionPricingModel.BSM,\n", + " endpoint_source=OptionSpotEndpointSource.QUOTE,\n", + " dividend_type=DivType.DISCRETE,\n", + " vol=None,\n", + " model_price=ModelPrice.MIDPOINT,\n", + " n_steps = 500\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "82738321", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 59, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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EKZNm0ZQUJCjzbpk3nvvPX7//XcaN25MQEAAd911F5GRkY42q0ZYvHgxp0+f5sknn3SpvzUpJR988AF79+7F29ub8PBw7rzzTnx8fFzi89JVrxs477XTfUSKsWfPHpYtW8Z1111Hjx49HG1OjfD111+zYsUKmjZtysyZM9m3bx/ff/89jz/+uEvcyPLz83nzzTc5e/YsM2bMwNfXl6VLl5KSksI///lPR5t3yeTm5vLKK6+QlZXFjTfeSGJiIps2bSI/P597772X6OhoR5tYbT788EP27t3LjBkzSElJYePGjWRlZXHHHXfQqVMnR5tXbU6ePMmSJUs4efIkKSkp3HfffQwePNglnP9Dhw6xaNEiPD09ue666zh27Bi//PILLVq0YNasWfVaoytfN3Dua6cjIsX44YcfiIiIoEePHhw6dIjt27cTERFBx44diYqKcrR5VSYhIYE//viD2267jWHDhgEqvB8XF1ciDF6fv8EkJiYSGxvL9OnTadu2LQCXX345GzZsoLCwEHf3+v0Wj42NJSkpifvvv5+WLVvSuXNnevTowb333svatWuZMmUKwcHBjjazSkgpyc/PZ//+/fTp04fOnTsDcNlllzFnzhw2bNhAUFBQvY2MnDp1iqCgICZMmMDWrVtZsmQJAwYMqPfvRdM0+f3332nWrBl333033t7e9OrViyZNmrB06VLS09MJDAx0tJnVxlWvGzj/tav/bl4NYJomeXl5pKWl0a1bN77++mv+9a9/ER8fz6pVq5g3bx5btmxxtJlVJiwsjLlz59qdECklfn5+hIeHs3fvXoB67YSAunYJCQn2D4vc3FxWr15NSEgIP/zwQ71PzM3IyCA5OZmWLVuWeMzf3589e/bYr2N9QghBVlYWZ8+epVWrVgAUFhbi6enJNddcQ3x8PNu3b3ewlVXH5tx36dKF8ePHExMTw7hx4xBCsHz58hLb1EcMwyAmJoZRo0bh7e1tfzw/Px9PT0+8vb0dnmtQFS68Fp07d3ap63ahzc587eq/q1cNPv/8c86dO0fTpk0ZPnw47u7ueHl5AbBp0yZCQ0N54IEH6NSpE25ubjz//PNs2rSJyMjIEjcEZ6MsXYA95CaEICAggMLCQgoKCoD6FREpS1/Lli3p0aMH77zzDs2aNWP37t107twZPz8/li1bxvbt27nuuuto06aNo82/KGXp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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "t.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45c1bb5d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "datetime\n", + "2025-05-23 7.451125\n", + "2025-05-27 8.498988\n", + "2025-05-28 7.873432\n", + "2025-05-29 7.674288\n", + "2025-05-30 7.673466\n", + " ... \n", + "2026-01-22 8.923519\n", + "2026-01-23 9.298345\n", + "2026-01-26 8.650853\n", + "2026-01-27 7.974164\n", + "2026-01-28 8.225831\n", + "Name: theoretical_price, Length: 171, dtype: float64" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p.timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fda35e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "p.timeseries.plot()\n", + "market.price.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2dc8a8bb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openhighlowclosevolumebid_sizeclosebidask_sizecloseaskmidpointweighted_midpoint
datetime
2025-05-230.08.7000.07.4500.0401.06.80325.08.107.4507.381956
2025-05-260.00.0000.00.0000.00.00.000.00.000.0000.000000
2025-05-270.08.9250.08.5000.0364.07.95304.09.058.5008.450599
2025-05-280.09.0250.07.8750.0414.07.50212.08.257.8757.753994
2025-05-290.08.5000.07.6750.055.07.05738.08.307.6758.213304
....................................
2026-01-220.09.5750.08.9250.0649.08.00742.09.858.9258.986844
2026-01-230.09.5500.09.3000.0579.08.90353.09.709.3009.203004
2026-01-260.09.2500.08.6500.0598.08.20571.09.108.6508.639607
2026-01-270.08.6250.07.9750.0671.07.30559.08.657.9757.913537
2026-01-280.010.8750.08.2250.0198.07.60771.08.858.2258.594582
\n", + "

179 rows × 11 columns

\n", + "
" + ], + "text/plain": [ + " open high low close volume bid_size closebid ask_size \\\n", + "datetime \n", + "2025-05-23 0.0 8.700 0.0 7.450 0.0 401.0 6.80 325.0 \n", + "2025-05-26 0.0 0.000 0.0 0.000 0.0 0.0 0.00 0.0 \n", + "2025-05-27 0.0 8.925 0.0 8.500 0.0 364.0 7.95 304.0 \n", + "2025-05-28 0.0 9.025 0.0 7.875 0.0 414.0 7.50 212.0 \n", + "2025-05-29 0.0 8.500 0.0 7.675 0.0 55.0 7.05 738.0 \n", + "... ... ... ... ... ... ... ... ... \n", + "2026-01-22 0.0 9.575 0.0 8.925 0.0 649.0 8.00 742.0 \n", + "2026-01-23 0.0 9.550 0.0 9.300 0.0 579.0 8.90 353.0 \n", + "2026-01-26 0.0 9.250 0.0 8.650 0.0 598.0 8.20 571.0 \n", + "2026-01-27 0.0 8.625 0.0 7.975 0.0 671.0 7.30 559.0 \n", + "2026-01-28 0.0 10.875 0.0 8.225 0.0 198.0 7.60 771.0 \n", + "\n", + " closeask midpoint weighted_midpoint \n", + "datetime \n", + "2025-05-23 8.10 7.450 7.381956 \n", + "2025-05-26 0.00 0.000 0.000000 \n", + "2025-05-27 9.05 8.500 8.450599 \n", + "2025-05-28 8.25 7.875 7.753994 \n", + "2025-05-29 8.30 7.675 8.213304 \n", + "... ... ... ... \n", + "2026-01-22 9.85 8.925 8.986844 \n", + "2026-01-23 9.70 9.300 9.203004 \n", + "2026-01-26 9.10 8.650 8.639607 \n", + "2026-01-27 8.65 7.975 7.913537 \n", + "2026-01-28 8.85 8.225 8.594582 \n", + "\n", + "[179 rows x 11 columns]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "market.timeseries\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/notebooks/timeseries.ipynb b/trade/datamanager/notebooks/timeseries.ipynb new file mode 100644 index 0000000..2223a7a --- /dev/null +++ b/trade/datamanager/notebooks/timeseries.ipynb @@ -0,0 +1,1204 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "80ad7787", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-03 22:16:35 trade.helpers.Logging INFO: Logging Root Directory: /Users/chiemelienwanisobi/cloned_repos/QuantTools/logs\n", + "2026-02-03 22:16:35 [test] trade.helpers.clear_cache INFO: No expired caches to delete on 2026-02-03.\n", + "2026-02-03 22:16:37 [test] dbase.DataAPI.ThetaData.proxy INFO: Refreshed proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-03 22:16:37 [test] dbase.DataAPI.ThetaData.proxy INFO: Using Proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-03 22:16:37 [test] dbase.DataAPI.ThetaData INFO: Using V2 of the ThetaData API\n", + "Fetching rates data from yfinance directly during market hours\n", + "YF.download() has changed argument auto_adjust default to True\n", + "\n", + "\n", + "Scheduled Data Requests will be saved to: /Users/chiemelienwanisobi/cloned_repos/QuantTools/module_test/raw_code/DataManagers/scheduler/requests.jsonl\n", + "2026-02-03 22:16:40 [test] DataManager.py CRITICAL: Using ProcessSaveManager for saving data.\n" + ] + } + ], + "source": [ + "from trade.datamanager import (\n", + " DividendDataManager,\n", + " SpotDataManager,\n", + " MarketTimeseries,\n", + " VolDataManager,\n", + " ForwardDataManager,\n", + " OptionSpotDataManager,\n", + " GreekDataManager,\n", + " RatesDataManager,\n", + " calculate_scenarios,\n", + ")\n", + "from trade.datamanager.utils.model import LoadRequest, _load_model_data_timeseries, ModelResultPack\n", + "from trade.datamanager.utils.date import DATE_HINT\n", + "from trade.datamanager._enums import SeriesId, OptionSpotEndpointSource, VolatilityModel, OptionPricingModel, ModelPrice, DivType\n", + "import pandas as pd\n", + "from datetime import datetime, time\n", + "from trade.datamanager.vars import get_times_series\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8b881e20", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-03 22:16:39.633954...\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-03 22:16:39.633954...\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-03 22:16:39.633954...\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 22:16:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-03 22:16:39.633954...\n" + ] + } + ], + "source": [ + "ts = get_times_series()\n", + "res = ts.get_timeseries(\"AAPL\", start_date=\"2026-01-01\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6cf7d4b3", + "metadata": {}, + "outputs": [], + "source": [ + "def load_full_option_data(\n", + " symbol: str,\n", + " *,\n", + " expiration: DATE_HINT,\n", + " strike: float,\n", + " right: str,\n", + " start_date: DATE_HINT = None,\n", + " end_date: DATE_HINT = None,\n", + " as_of: DATE_HINT = None,\n", + " rt: bool = False,\n", + "\n", + " ## Optional parameters. If not passed will refer to global defaults\n", + " ## found in OptionConfig\n", + " series_id: SeriesId = None,\n", + " dividend_type: DivType = None,\n", + " endpoint_source: OptionSpotEndpointSource = None,\n", + " vol_model: VolatilityModel = None,\n", + " market_model: OptionPricingModel = None,\n", + " model_price: ModelPrice = None,\n", + ") -> ModelResultPack:\n", + "\n", + " if start_date and end_date:\n", + " ts_start = pd.to_datetime(start_date)\n", + " ts_end = pd.to_datetime(end_date)\n", + " as_of = None\n", + " rt = False\n", + "\n", + " elif as_of:\n", + " ts_start = None\n", + " ts_end = None\n", + " as_of = pd.to_datetime(as_of)\n", + " rt = False\n", + "\n", + " elif rt:\n", + " ts_start = None\n", + " ts_end = None\n", + " as_of = None\n", + " rt = True\n", + " \n", + " request = LoadRequest(\n", + " symbol=symbol,\n", + " start_date=ts_start,\n", + " end_date=ts_end,\n", + " as_of=as_of,\n", + " expiration=expiration,\n", + " strike=strike,\n", + " right=right,\n", + " series_id=series_id,\n", + " dividend_type=dividend_type,\n", + " endpoint_source=endpoint_source,\n", + " vol_model=vol_model,\n", + " market_model=market_model,\n", + " model_price=model_price,\n", + " load_spot=True,\n", + " load_dividend=True,\n", + " load_forward=True,\n", + " load_option_spot=True,\n", + " load_vol=True,\n", + " load_greek=True,\n", + " load_rates=True,\n", + " undo_adjust=True,\n", + " rt=rt,\n", + " )\n", + " data_packet = _load_model_data_timeseries(request)\n", + " return data_packet\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "26dd8ae0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-03 22:18:41 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-03 22:18:41 [test] trade.datamanager.dividend INFO: Using provided dividend_type: DivType.CONTINUOUS\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AMD\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2017-01-01 to 2026-02-03 22:16:39.633954...\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: AMD. Fetching missing dates: [Timestamp('2017-01-03 00:00:00'), Timestamp('2017-01-04 00:00:00'), Timestamp('2017-01-05 00:00:00'), Timestamp('2017-01-06 00:00:00'), Timestamp('2017-01-09 00:00:00'), Timestamp('2017-01-10 00:00:00'), Timestamp('2017-01-11 00:00:00'), Timestamp('2017-01-12 00:00:00'), Timestamp('2017-01-13 00:00:00'), Timestamp('2017-01-17 00:00:00'), Timestamp('2017-01-18 00:00:00'), Timestamp('2017-01-19 00:00:00'), Timestamp('2017-01-20 00:00:00'), Timestamp('2017-01-23 00:00:00'), Timestamp('2017-01-24 00:00:00'), Timestamp('2017-01-25 00:00:00'), Timestamp('2017-01-26 00:00:00'), Timestamp('2017-01-27 00:00:00'), Timestamp('2017-01-30 00:00:00'), Timestamp('2017-01-31 00:00:00'), Timestamp('2017-02-01 00:00:00'), Timestamp('2017-02-02 00:00:00'), Timestamp('2017-02-03 00:00:00'), Timestamp('2017-02-06 00:00:00'), Timestamp('2017-02-07 00:00:00'), Timestamp('2017-02-08 00:00:00'), Timestamp('2017-02-09 00:00:00'), Timestamp('2017-02-10 00:00:00'), Timestamp('2017-02-13 00:00:00'), Timestamp('2017-02-14 00:00:00'), Timestamp('2017-02-15 00:00:00'), Timestamp('2017-02-16 00:00:00'), Timestamp('2017-02-17 00:00:00'), Timestamp('2017-02-21 00:00:00'), Timestamp('2017-02-22 00:00:00'), Timestamp('2017-02-23 00:00:00'), Timestamp('2017-02-24 00:00:00'), Timestamp('2017-02-27 00:00:00'), Timestamp('2017-02-28 00:00:00'), Timestamp('2017-03-01 00:00:00'), Timestamp('2017-03-02 00:00:00'), Timestamp('2017-03-03 00:00:00'), Timestamp('2017-03-06 00:00:00'), Timestamp('2017-03-07 00:00:00'), Timestamp('2017-03-08 00:00:00'), Timestamp('2017-03-09 00:00:00'), Timestamp('2017-03-10 00:00:00'), Timestamp('2017-03-13 00:00:00'), Timestamp('2017-03-14 00:00:00'), Timestamp('2017-03-15 00:00:00'), Timestamp('2017-03-16 00:00:00'), Timestamp('2017-03-17 00:00:00'), Timestamp('2017-03-20 00:00:00'), Timestamp('2017-03-21 00:00:00'), Timestamp('2017-03-22 00:00:00'), Timestamp('2017-03-23 00:00:00'), Timestamp('2017-03-24 00:00:00'), Timestamp('2017-03-27 00:00:00'), Timestamp('2017-03-28 00:00:00'), Timestamp('2017-03-29 00:00:00'), Timestamp('2017-03-30 00:00:00')]\n", + "2026-02-03 22:18:42 [test] trade.datamanager.market_data_helpers.dividends INFO: Ticker AMD found in dividend cache.\n", + "2026-02-03 22:18:42 [test] trade.datamanager.market_data INFO: Loaded dividend data for symbol AMD into cache.\n", + "2026-02-03 22:18:42 [test] trade.datamanager.rates INFO: Cache hit for risk-free rate timeseries key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-03 22:18:42 [test] trade.datamanager.rates INFO: Cache fully covers requested date range for risk-free rate timeseries. Key: symbol:^IRX|interval:eod|artifact_type:rates|series_id:hist|fn_interval:1D\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-02...\n", + "2026-02-03 22:18:42 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AMD\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 00:00:00 to 2026-02-02 00:00:00...\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 00:00:00 to 2026-02-02 00:00:00...\n", + "2026-02-03 22:18:42 [test] trade.datamanager.vars INFO: Timeseries for AMD already loaded.\n", + "2026-02-03 22:18:42 [test] trade.datamanager.forward INFO: Cache partially covers requested date range for forward timeseries. Key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1. Fetching missing dates: [Timestamp('2026-01-05 00:00:00'), Timestamp('2026-01-06 00:00:00'), Timestamp('2026-01-07 00:00:00'), Timestamp('2026-01-08 00:00:00'), Timestamp('2026-01-09 00:00:00'), Timestamp('2026-01-12 00:00:00'), Timestamp('2026-01-13 00:00:00'), Timestamp('2026-01-14 00:00:00'), Timestamp('2026-01-15 00:00:00'), Timestamp('2026-01-16 00:00:00'), Timestamp('2026-01-20 00:00:00'), Timestamp('2026-01-21 00:00:00'), Timestamp('2026-01-22 00:00:00'), Timestamp('2026-01-23 00:00:00'), Timestamp('2026-01-26 00:00:00'), Timestamp('2026-01-27 00:00:00'), Timestamp('2026-01-28 00:00:00'), Timestamp('2026-01-29 00:00:00'), Timestamp('2026-01-30 00:00:00'), Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:forward|series_id:hist|dividend_type:CONTINUOUS|maturity:2026-09-18|use_chain_spot:1 to avoid saving partial day data.\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 00:00:00 to 2026-02-02 00:00:00...\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-01 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C280\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: symbol:AMD|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:280\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 00:00:00 to 2026-02-02 00:00:00...\n", + "2026-02-03 22:18:42 [test] trade.datamanager.option_spot INFO: Cache hit for option spot timeseries key: symbol:AMD|interval:eod|artifact_type:option_spot|series_id:hist|endpoint_source:EOD|expiration:20260918T000000|right:C|strike:280\n", + "2026-02-03 22:18:42 [test] trade.datamanager.vol INFO: VolDm Using specified dividend type: DivType.CONTINUOUS\n", + "2026-02-03 22:18:42 [test] trade.datamanager.vol INFO: VolDm Using model price: ModelPrice.MIDPOINT\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-01 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C280\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates: [Timestamp('2026-01-05 00:00:00'), Timestamp('2026-01-06 00:00:00'), Timestamp('2026-01-07 00:00:00'), Timestamp('2026-01-08 00:00:00'), Timestamp('2026-01-09 00:00:00'), Timestamp('2026-01-12 00:00:00'), Timestamp('2026-01-13 00:00:00'), Timestamp('2026-01-14 00:00:00'), Timestamp('2026-01-15 00:00:00'), Timestamp('2026-01-16 00:00:00'), Timestamp('2026-01-20 00:00:00'), Timestamp('2026-01-21 00:00:00'), Timestamp('2026-01-22 00:00:00'), Timestamp('2026-01-23 00:00:00'), Timestamp('2026-01-26 00:00:00'), Timestamp('2026-01-27 00:00:00'), Timestamp('2026-01-28 00:00:00'), Timestamp('2026-01-29 00:00:00'), Timestamp('2026-01-30 00:00:00'), Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates.\n", + "2026-02-03 22:18:42 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:iv|series_id:hist|american:1|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|n_steps:100|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market to avoid saving partial day data.\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-02...\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Using cached date range for 2026-01-01 00:00:00 - 2026-02-02 00:00:00 and option tick AMD20260918C280\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Cache partially covers requested date range for timeseries data structure. Key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates: [Timestamp('2026-01-05 00:00:00'), Timestamp('2026-01-06 00:00:00'), Timestamp('2026-01-07 00:00:00'), Timestamp('2026-01-08 00:00:00'), Timestamp('2026-01-09 00:00:00'), Timestamp('2026-01-12 00:00:00'), Timestamp('2026-01-13 00:00:00'), Timestamp('2026-01-14 00:00:00'), Timestamp('2026-01-15 00:00:00'), Timestamp('2026-01-16 00:00:00'), Timestamp('2026-01-20 00:00:00'), Timestamp('2026-01-21 00:00:00'), Timestamp('2026-01-22 00:00:00'), Timestamp('2026-01-23 00:00:00'), Timestamp('2026-01-26 00:00:00'), Timestamp('2026-01-27 00:00:00'), Timestamp('2026-01-28 00:00:00'), Timestamp('2026-01-29 00:00:00'), Timestamp('2026-01-30 00:00:00'), Timestamp('2026-02-02 00:00:00')]\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Cache partially covers requested date range. Key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market. Fetching missing dates.\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils WARNING: No data requested to load in _load_model_data_timeseries().\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Cutting off today's data for key: symbol:AMD|interval:eod|artifact_type:greeks|series_id:hist|dividend_type:continuous|endpoint_source:eod|expiration:20260918T000000|model_price:midpoint|option_pricing_model:Binomial|right:C|strike:280|volatility_model:market to avoid saving partial day data.\n", + "2026-02-03 22:18:45 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-01-01 to 2026-02-02...\n" + ] + } + ], + "source": [ + "info = {\n", + " \"symbol\": \"AMD\",\n", + " \"expiration\": \"2026-09-18\",\n", + " \"right\": \"C\",\n", + " \"strike\": 280.0,\n", + "}\n", + "pack = load_full_option_data(\n", + " symbol=info[\"symbol\"],\n", + " expiration=info[\"expiration\"],\n", + " strike=info[\"strike\"],\n", + " right=info[\"right\"],\n", + " start_date=\"2026-01-01\",\n", + " end_date=\"2026-02-02\",\n", + " dividend_type=DivType.CONTINUOUS\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8f6aa4b3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
vegathetarhovolgadeltagamma
datetime
2026-01-020.733965-0.0856890.488369-0.0014060.4164950.003832
2026-01-050.726300-0.0861820.468530-0.0014080.4078930.003841
2026-01-060.717364-0.0839500.424521-0.0015230.3774890.003935
2026-01-070.631619-0.0730120.393452-0.0013900.3539630.004015
2026-01-080.626035-0.0705270.355758-0.0015250.3251400.004077
2026-01-090.623984-0.0697340.342692-0.0015750.3149380.004103
2026-01-120.625102-0.0716060.364971-0.0014720.3340300.004105
2026-01-130.717705-0.0855640.446714-0.0014270.3956250.003977
2026-01-140.718805-0.0871340.461712-0.0013630.4082230.003929
2026-01-150.721653-0.0895670.486846-0.0012700.4280460.003850
2026-01-160.724781-0.0910310.507732-0.0012020.4427450.003810
2026-01-200.718297-0.0931530.500452-0.0011640.4448490.003778
2026-01-210.798398-0.1063840.601674-0.0010640.5123280.003498
2026-01-220.801387-0.1083380.621356-0.0010060.5262220.003430
2026-01-230.839768-0.1144700.651872-0.0010000.5465260.003325
2026-01-260.792361-0.1081070.597352-0.0010180.5158180.003503
2026-01-270.791854-0.1082520.598902-0.0010040.5173730.003510
2026-01-280.791132-0.1087580.600382-0.0009910.5195110.003507
2026-01-290.789461-0.1078220.594896-0.0009970.5154790.003559
2026-01-300.768982-0.1035670.504380-0.0011950.4578900.003786
2026-02-020.775850-0.1071030.551133-0.0010540.4928720.003665
\n", + "
" + ], + "text/plain": [ + " vega theta rho volga delta gamma\n", + "datetime \n", + "2026-01-02 0.733965 -0.085689 0.488369 -0.001406 0.416495 0.003832\n", + "2026-01-05 0.726300 -0.086182 0.468530 -0.001408 0.407893 0.003841\n", + "2026-01-06 0.717364 -0.083950 0.424521 -0.001523 0.377489 0.003935\n", + "2026-01-07 0.631619 -0.073012 0.393452 -0.001390 0.353963 0.004015\n", + "2026-01-08 0.626035 -0.070527 0.355758 -0.001525 0.325140 0.004077\n", + "2026-01-09 0.623984 -0.069734 0.342692 -0.001575 0.314938 0.004103\n", + "2026-01-12 0.625102 -0.071606 0.364971 -0.001472 0.334030 0.004105\n", + "2026-01-13 0.717705 -0.085564 0.446714 -0.001427 0.395625 0.003977\n", + "2026-01-14 0.718805 -0.087134 0.461712 -0.001363 0.408223 0.003929\n", + "2026-01-15 0.721653 -0.089567 0.486846 -0.001270 0.428046 0.003850\n", + "2026-01-16 0.724781 -0.091031 0.507732 -0.001202 0.442745 0.003810\n", + "2026-01-20 0.718297 -0.093153 0.500452 -0.001164 0.444849 0.003778\n", + "2026-01-21 0.798398 -0.106384 0.601674 -0.001064 0.512328 0.003498\n", + "2026-01-22 0.801387 -0.108338 0.621356 -0.001006 0.526222 0.003430\n", + "2026-01-23 0.839768 -0.114470 0.651872 -0.001000 0.546526 0.003325\n", + "2026-01-26 0.792361 -0.108107 0.597352 -0.001018 0.515818 0.003503\n", + "2026-01-27 0.791854 -0.108252 0.598902 -0.001004 0.517373 0.003510\n", + "2026-01-28 0.791132 -0.108758 0.600382 -0.000991 0.519511 0.003507\n", + "2026-01-29 0.789461 -0.107822 0.594896 -0.000997 0.515479 0.003559\n", + "2026-01-30 0.768982 -0.103567 0.504380 -0.001195 0.457890 0.003786\n", + "2026-02-02 0.775850 -0.107103 0.551133 -0.001054 0.492872 0.003665" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pack.greek.timeseries" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3e2cc4fe", + "metadata": {}, + "outputs": [], + "source": [ + "def _assert_option_parameters_available(strike: float, expiry: DATE_HINT, option_type: str) -> None:\n", + " if strike is None:\n", + " raise ValueError(\"Strike price must be provided.\")\n", + " if expiry is None:\n", + " raise ValueError(\"Expiry date must be provided.\")\n", + " if option_type is None:\n", + " raise ValueError(\"Option type (call/put) must be provided.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "b865e840", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Callable, Any, Optional\n", + "from functools import wraps\n", + "import inspect\n", + "\n", + "class TimeseriesAdapter:\n", + " \"\"\"Adapter that provides a consistent interface for any DataManager.\n", + " \n", + " Maps standardized method names (rt, get_at_time, get_timeseries) to \n", + " the actual underlying DataManager methods while preserving original\n", + " docstrings, signatures, and type hints.\n", + " \"\"\"\n", + " \n", + " def __init__(\n", + " self,\n", + " manager: Any,\n", + " rt_method: Optional[str] = \"rt\",\n", + " get_at_time_method: Optional[str] = None,\n", + " get_timeseries_method: Optional[str] = None,\n", + " ):\n", + " \"\"\"Initialize adapter with method name mappings.\n", + " \n", + " Args:\n", + " manager: The underlying DataManager instance\n", + " rt_method: Name of the real-time method (default: \"rt\")\n", + " get_at_time_method: Name of the get-at-time method (e.g., \"get_at_time\", \"get_at_time_implied_volatility\")\n", + " get_timeseries_method: Name of the timeseries method (e.g., \"get_spot_timeseries\", \"get_implied_volatility_timeseries\")\n", + " \"\"\"\n", + " self._manager = manager\n", + " self._rt_method = rt_method\n", + " self._get_at_time_method = get_at_time_method\n", + " self._get_timeseries_method = get_timeseries_method\n", + " \n", + " # Create wrapper methods with copied metadata\n", + " self._create_wrapper_method(\"rt\", rt_method)\n", + " self._create_wrapper_method(\"get_at_time\", get_at_time_method)\n", + " self._create_wrapper_method(\"get_timeseries\", get_timeseries_method)\n", + " \n", + " def _create_wrapper_method(self, wrapper_name: str, underlying_method_name: Optional[str]):\n", + " \"\"\"Create a wrapper method that copies docstring and signature from underlying method.\n", + " \n", + " Args:\n", + " wrapper_name: Name of the wrapper method on this adapter\n", + " underlying_method_name: Name of the actual method on the underlying manager\n", + " \"\"\"\n", + " if not underlying_method_name or not hasattr(self._manager, underlying_method_name):\n", + " return\n", + " \n", + " underlying_method = getattr(self._manager, underlying_method_name)\n", + " \n", + " # Create wrapper function that calls the underlying method\n", + " def wrapper(*args, **kwargs):\n", + " return underlying_method(*args, **kwargs)\n", + " \n", + " # Copy metadata for proper introspection\n", + " wrapper.__name__ = wrapper_name\n", + " wrapper.__doc__ = underlying_method.__doc__\n", + " wrapper.__wrapped__ = underlying_method # Allows ? to find original source\n", + " \n", + " # Copy module and qualname for correct file location display\n", + " if hasattr(underlying_method, '__module__'):\n", + " wrapper.__module__ = underlying_method.__module__\n", + " if hasattr(underlying_method, '__qualname__'):\n", + " # Keep the wrapper name but use the module path\n", + " wrapper.__qualname__ = f\"{underlying_method.__qualname__.rsplit('.', 1)[0]}.{wrapper_name}\"\n", + " \n", + " # Copy signature\n", + " try:\n", + " wrapper.__signature__ = inspect.signature(underlying_method)\n", + " except (ValueError, TypeError):\n", + " pass\n", + " \n", + " # Copy annotations if available\n", + " if hasattr(underlying_method, '__annotations__'):\n", + " wrapper.__annotations__ = underlying_method.__annotations__.copy()\n", + " \n", + " # Set as instance attribute (overrides class method)\n", + " setattr(self, wrapper_name, wrapper)\n", + " \n", + " def rt(self, *args, **kwargs):\n", + " \"\"\"Call the underlying manager's real-time method.\"\"\"\n", + " if self._rt_method and hasattr(self._manager, self._rt_method):\n", + " method = getattr(self._manager, self._rt_method)\n", + " return method(*args, **kwargs)\n", + " raise NotImplementedError(f\"{self._manager.__class__.__name__} does not support rt()\")\n", + " \n", + " def get_at_time(self, *args, **kwargs):\n", + " \"\"\"Call the underlying manager's get-at-time method.\"\"\"\n", + " if self._get_at_time_method and hasattr(self._manager, self._get_at_time_method):\n", + " method = getattr(self._manager, self._get_at_time_method)\n", + " return method(*args, **kwargs)\n", + " raise NotImplementedError(f\"{self._manager.__class__.__name__} does not support get_at_time()\")\n", + " \n", + " def get_timeseries(self, *args, **kwargs):\n", + " \"\"\"Call the underlying manager's timeseries method.\"\"\"\n", + " if self._get_timeseries_method and hasattr(self._manager, self._get_timeseries_method):\n", + " method = getattr(self._manager, self._get_timeseries_method)\n", + " return method(*args, **kwargs)\n", + " raise NotImplementedError(f\"{self._manager.__class__.__name__} does not support get_timeseries()\")\n", + " \n", + " def __getattr__(self, name: str):\n", + " \"\"\"Pass through any other attribute access to the underlying manager.\"\"\"\n", + " return getattr(self._manager, name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cf1aa8a8", + "metadata": {}, + "outputs": [], + "source": [ + "class TimeseriesDataManager:\n", + " \"\"\"Unified interface for all data managers with consistent method naming.\n", + " \n", + " Each data manager is wrapped with a TimeseriesAdapter that maps standardized\n", + " method names (rt, get_at_time, get_timeseries) to the actual underlying methods.\n", + " \n", + " Examples:\n", + " >>> # Basic spot data access\n", + " >>> ts = TimeseriesDataManager(\"AAPL\")\n", + " >>> spot_result = ts.spot.rt()\n", + " >>> spot_series = ts.spot.get_timeseries(start_date=\"2025-01-01\", end_date=\"2025-01-31\")\n", + " \n", + " >>> # Options data - pass parameters explicitly\n", + " >>> ts = TimeseriesDataManager(\"AAPL\")\n", + " >>> vol_result = ts.vol.rt(strike=150.0, expiration=\"2025-06-20\", right=\"call\")\n", + " >>> greeks = ts.greeks.get_timeseries(\n", + " ... start_date=\"2025-01-01\", end_date=\"2025-01-31\",\n", + " ... strike=150.0, expiration=\"2025-06-20\", right=\"call\"\n", + " ... )\n", + " \"\"\"\n", + " \n", + " def __init__(self, symbol: str):\n", + " \"\"\"Initialize unified timeseries data manager.\n", + " \n", + " Args:\n", + " symbol: Ticker symbol (e.g., \"AAPL\")\n", + " \"\"\"\n", + " self.symbol = symbol\n", + " \n", + " # Initialize underlying managers\n", + " self._spot_manager = SpotDataManager(symbol=symbol)\n", + " self._vol_manager = VolDataManager(symbol=symbol)\n", + " self._dividend_manager = DividendDataManager(symbol=symbol)\n", + " self._forward_manager = ForwardDataManager(symbol=symbol)\n", + " self._option_spot_manager = OptionSpotDataManager(symbol=symbol)\n", + " self._greeks_manager = GreekDataManager(symbol=symbol)\n", + " self._rates_manager = RatesDataManager()\n", + " \n", + " @property\n", + " def spot(self) -> TimeseriesAdapter:\n", + " \"\"\"Access spot price data with standardized interface.\n", + " \n", + " Methods:\n", + " - rt(): Get real-time spot price\n", + " - get_at_time(date): Get spot price at specific date\n", + " - get_timeseries(start_date, end_date, undo_adjust=True): Get spot price series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._spot_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=\"get_at_time\",\n", + " get_timeseries_method=\"get_spot_timeseries\"\n", + " )\n", + " \n", + " @property\n", + " def vol(self) -> TimeseriesAdapter:\n", + " \"\"\"Access implied volatility data with standardized interface.\n", + " \n", + " Methods:\n", + " - rt(strike, expiration, right, ...): Get real-time implied volatility\n", + " - get_at_time(date, strike, expiration, right, ...): Get implied vol at specific date\n", + " - get_timeseries(start_date, end_date, strike, expiration, right, ...): Get implied vol series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._vol_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=\"get_at_time_implied_volatility\",\n", + " get_timeseries_method=\"get_implied_volatility_timeseries\"\n", + " )\n", + " \n", + " @property\n", + " def greeks(self) -> TimeseriesAdapter:\n", + " \"\"\"Access option greeks data with standardized interface.\n", + " \n", + " Requires: strike, expiration, right parameters set in constructor\n", + " \n", + " Methods:\n", + " - rt(strike, expiration, right, ...): Get real-time option greeks\n", + " - get_at_time(date, strike, expiration, right, ...): Get greeks at specific date\n", + " - get_timeseries(start_date, end_date, strike, expiration, right, ...): Get greeks series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._greeks_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=\"get_at_time_greeks\",\n", + " get_timeseries_method=\"get_greeks_timeseries\"\n", + " )\n", + " \n", + " @property\n", + " def forward(self) -> TimeseriesAdapter:\n", + " \"\"\"Access forward price data with standardized interface.\n", + " \n", + " Methods:\n", + " - rt(maturity_date): Get real-time forward price\n", + " - get_timeseries(start_date, end_date, maturity_date): Get forward price series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._forward_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=None, # Forward doesn't have get_at_time\n", + " get_timeseries_method=\"get_forward_timeseries\"\n", + " )\n", + " \n", + " @property\n", + " def dividend(self) -> TimeseriesAdapter:\n", + " \"\"\"Access dividend data with standardized interface.\n", + " \n", + " Methods:\n", + " - rt(maturity_date): Get real-time dividend schedule\n", + " - get_timeseries(start_date, end_date, maturity_date): Get dividend series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._dividend_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=None, # Dividend doesn't have get_at_time\n", + " get_timeseries_method=\"get_schedule_timeseries\"\n", + " )\n", + " \n", + " @property\n", + " def rates(self) -> TimeseriesAdapter:\n", + " \"\"\"Access risk-free rate data with standardized interface.\n", + " \n", + " Methods:\n", + " - rt(): Get real-time risk-free rate\n", + " - get_timeseries(start_date, end_date): Get rate series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._rates_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=None, # Rates doesn't have get_at_time\n", + " get_timeseries_method=\"get_risk_free_rate_timeseries\"\n", + " )\n", + " \n", + " @property\n", + " def option_spot(self) -> TimeseriesAdapter:\n", + " \"\"\"Access option market price data with standardized interface.\n", + " \n", + " Requires: strike, expiration, right parameters set in constructor\n", + " \n", + " Methods:\n", + " - rt(strike, expiration, right, ...): Get real-time option market price\n", + " - get_at_time(date, strike, expiration, right, ...): Get option price at specific date\n", + " - get_timeseries(start_date, end_date, strike, expiration, right, ...): Get option price series\n", + " \"\"\"\n", + " return TimeseriesAdapter(\n", + " manager=self._option_spot_manager,\n", + " rt_method=\"rt\",\n", + " get_at_time_method=\"get_option_spot_at_time\",\n", + " get_timeseries_method=\"get_option_spot_timeseries\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "723f3bb4", + "metadata": {}, + "source": [ + "## Usage Examples" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "9da18224", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-03 21:07:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-03 21:07:48.701764 to 2026-02-03 21:07:48.701764...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-03 21:07:48.701764 to 2026-02-03 21:07:48.701764...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-03 21:07:48.701764 to 2026-02-03 21:07:48.701764...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2026-02-03 21:07:48.701764 to 2026-02-03 20:43:58.921712...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-15 00:00:00 to 2025-01-15 00:00:00...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-15 00:00:00 to 2025-01-15 00:00:00...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-15 00:00:00 to 2025-01-15 00:00:00...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-15 00:00:00 to 2026-02-03 20:43:58.921712...\n", + "2026-02-03 21:07:48 [test] trade.datamanager.vars INFO: Timeseries for AAPL already loaded.\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:48 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2025-01-31...\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2025-01-31...\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2025-01-31...\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: AAPL\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2026-02-03 20:43:58.921712...\n", + "2026-02-03 21:07:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2025-01-01 to 2025-01-31...\n", + "Spot RT: SpotResult(symbol='AAPL', key=None, is_empty=False, undo_adjust=True)\n", + "Spot at time: SpotResult(symbol='AAPL', key=None, is_empty=False, undo_adjust=True)\n", + "Spot series: SpotResult(symbol='AAPL', key=None, is_empty=False, undo_adjust=True)\n" + ] + } + ], + "source": [ + "# Example 1: Spot data with consistent interface\n", + "ts = TimeseriesDataManager(\"AAPL\")\n", + "\n", + "# All these work the same way:\n", + "spot_rt = ts.spot.rt()\n", + "spot_at_time = ts.spot.get_at_time(date=\"2025-01-15\")\n", + "spot_series = ts.spot.get_timeseries(start_date=\"2025-01-01\", end_date=\"2025-01-31\", undo_adjust=True)\n", + "\n", + "print(\"Spot RT:\", spot_rt)\n", + "print(\"Spot at time:\", spot_at_time)\n", + "print(\"Spot series:\", spot_series)" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "b0dbe47b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mSignature:\u001b[0m\n", + "\u001b[0mts\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0moption_spot\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstrike\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mfloat\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mright\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mexpiration\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mOptionSpotResult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m\n", + "Fetches real-time option spot price from Thetadata Quote endpoint.\n", + "\n", + "Retrieves the most recent OHLC data for a specific option contract using\n", + "Thetadata's Quote endpoint.\n", + "\n", + "Args:\n", + " strike: Option strike price.\n", + " right: Option type (\"C\" for call, \"P\" for put).\n", + " expiration: Option expiration date.\n", + "\n", + "Returns:\n", + " OptionSpotResult containing daily_option_spot DataFrame with OHLC data,\n", + " plus metadata (key, endpoint_source).\n", + "\u001b[0;31mFile:\u001b[0m ~/cloned_repos/QuantTools/trade/datamanager/option_spot.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + } + ], + "source": [ + "ts.option_spot.rt?" + ] + }, + { + "cell_type": "markdown", + "id": "25bb26cc", + "metadata": {}, + "source": [ + "### Test: Verify docstrings are preserved" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "79216170", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Docstring for ts.spot.get_timeseries():\n", + "================================================================================\n", + "Returns spot or chain_spot price series for date range from MarketTimeseries.\n", + "\n", + " Retrieves closing prices from the global MarketTimeseries cache. Returns either\n", + " split-adjusted (chain_spot) or unadjusted (spot) prices based on undo_adjust flag.\n", + "\n", + " Args:\n", + " start_date: Start of date range (YYYY-MM-DD string or datetime).\n", + " end_date: End of date range (YYYY-MM-DD string or datetime).\n", + " undo_adjust: If True, returns split-adjusted chain_spot prices.\n", + " If False, returns unadjusted spot prices.\n", + "\n", + " Returns:\n", + " SpotResult containing daily_spot Series indexed by datetime, plus metadata\n", + " (undo_adjust flag and cache key).\n", + "\n", + " Examples:\n", + " >>> spot_mgr = SpotDataManager(\"AAPL\")\n", + " >>> # Get split-adjusted prices (recommended for backtesting)\n", + " >>> result = spot_mgr.get_spot_timeseries(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... undo_adjust=True\n", + " ... )\n", + " >>> chain_spot = result.daily_spot\n", + " >>> print(chain_spot.head())\n", + " datetime\n", + " 2025-01-02 155.32\n", + " 2025-01-03 156.01\n", + " ...\n", + "\n", + " >>> # Get unadjusted prices (for real-time pricing)\n", + " >>> result = spot_mgr.get_spot_timeseries(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... undo_adjust=False\n", + " ... )\n", + " >>> spot = result.daily_spot\n", + "\n", + " Notes:\n", + " - chain_spot: Split-adjusted prices (use with undo_adjust=True dividends)\n", + " - spot: Unadjusted prices (use with undo_adjust=False dividends)\n", + " - Data loaded directly from global TS cache (no additional caching)\n", + " - Automatically filters to business days (excludes weekends/holidays)\n", + " \n", + "\n", + "================================================================================\n", + "\n", + "Signature:\n", + "(start_date: Union[datetime.datetime, str], end_date: Union[datetime.datetime, str], undo_adjust: bool = True) -> trade.datamanager.result.SpotResult\n" + ] + } + ], + "source": [ + "# Check that get_timeseries has the original docstring from get_spot_timeseries\n", + "print(\"Docstring for ts.spot.get_timeseries():\")\n", + "print(\"=\" * 80)\n", + "print(ts.spot.get_timeseries.__doc__)\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"\\nSignature:\")\n", + "print(inspect.signature(ts.spot.get_timeseries))" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "ac9ecf67", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original method docstring:\n", + "================================================================================\n", + "Returns spot or chain_spot price series for date range from MarketTimeseries.\n", + "\n", + " Retrieves closing prices from the global MarketTimeseries cache. Returns either\n", + " split-adjusted (chain_spot) or unadjusted (spot) prices based on undo_adjust flag.\n", + "\n", + " Args:\n", + " start_date: Start of date range (YYYY-MM-DD string or datetime).\n", + " end_date: End of date range (YYYY-MM-DD string or datetime).\n", + " undo_adjust: If True, returns split-adjusted chain_spot prices.\n", + " If False, returns unadjusted spot prices.\n", + "\n", + " Returns:\n", + " SpotResult containing daily_spot Series indexed by datetime, plus metadata\n", + " (undo_adjust flag and cache key).\n", + "\n", + " Examples:\n", + " >>> spot_mgr = SpotDataManager(\"AAPL\")\n", + " >>> # Get split-adjusted prices (recommended for backtesting)\n", + " >>> result = spot_mgr.get_spot_timeseries(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... undo_adjust=True\n", + " ... )\n", + " >>> chain_spot = result.daily_spot\n", + " >>> print(chain_spot.head())\n", + " datetime\n", + " 2025-01-02 155.32\n", + " 2025-01-03 156.01\n", + " ...\n", + "\n", + " >>> # Get unadjusted prices (for real-time pricing)\n", + " >>> result = spot_mgr.get_spot_timeseries(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... undo_adjust=False\n", + " ... )\n", + " >>> spot = result.daily_spot\n", + "\n", + " Notes:\n", + " - chain_spot: Split-adjusted prices (use with undo_adjust=True dividends)\n", + " - spot: Unadjusted prices (use with undo_adjust=False dividends)\n", + " - Data loaded directly from global TS cache (no additional caching)\n", + " - Automatically filters to business days (excludes weekends/holidays)\n", + " \n", + "\n", + "================================================================================\n", + "\n", + "Original Signature:\n", + "(start_date: Union[datetime.datetime, str], end_date: Union[datetime.datetime, str], undo_adjust: bool = True) -> trade.datamanager.result.SpotResult\n" + ] + } + ], + "source": [ + "# Compare with original method\n", + "print(\"Original method docstring:\")\n", + "print(\"=\" * 80)\n", + "print(ts.spot._manager.get_spot_timeseries.__doc__)\n", + "print(\"\\n\" + \"=\" * 80)\n", + "print(\"\\nOriginal Signature:\")\n", + "print(inspect.signature(ts.spot._manager.get_spot_timeseries))" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "c8b66bc6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0;31mSignature:\u001b[0m\n", + "\u001b[0mts\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mspot\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget_timeseries\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mstart_date\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mend_date\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mUnion\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatetime\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m \u001b[0mundo_adjust\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mbool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", + "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0mtrade\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdatamanager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mSpotResult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mDocstring:\u001b[0m\n", + "Returns spot or chain_spot price series for date range from MarketTimeseries.\n", + "\n", + "Retrieves closing prices from the global MarketTimeseries cache. Returns either\n", + "split-adjusted (chain_spot) or unadjusted (spot) prices based on undo_adjust flag.\n", + "\n", + "Args:\n", + " start_date: Start of date range (YYYY-MM-DD string or datetime).\n", + " end_date: End of date range (YYYY-MM-DD string or datetime).\n", + " undo_adjust: If True, returns split-adjusted chain_spot prices.\n", + " If False, returns unadjusted spot prices.\n", + "\n", + "Returns:\n", + " SpotResult containing daily_spot Series indexed by datetime, plus metadata\n", + " (undo_adjust flag and cache key).\n", + "\n", + "Examples:\n", + " >>> spot_mgr = SpotDataManager(\"AAPL\")\n", + " >>> # Get split-adjusted prices (recommended for backtesting)\n", + " >>> result = spot_mgr.get_spot_timeseries(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... undo_adjust=True\n", + " ... )\n", + " >>> chain_spot = result.daily_spot\n", + " >>> print(chain_spot.head())\n", + " datetime\n", + " 2025-01-02 155.32\n", + " 2025-01-03 156.01\n", + " ...\n", + "\n", + " >>> # Get unadjusted prices (for real-time pricing)\n", + " >>> result = spot_mgr.get_spot_timeseries(\n", + " ... start_date=\"2025-01-01\",\n", + " ... end_date=\"2025-01-31\",\n", + " ... undo_adjust=False\n", + " ... )\n", + " >>> spot = result.daily_spot\n", + "\n", + "Notes:\n", + " - chain_spot: Split-adjusted prices (use with undo_adjust=True dividends)\n", + " - spot: Unadjusted prices (use with undo_adjust=False dividends)\n", + " - Data loaded directly from global TS cache (no additional caching)\n", + " - Automatically filters to business days (excludes weekends/holidays)\n", + "\u001b[0;31mFile:\u001b[0m /var/folders/j0/80hkbygd4lb27h9mw76gqzpw0000gn/T/ipykernel_14877/3849854026.py\n", + "\u001b[0;31mType:\u001b[0m function" + ] + } + ], + "source": [ + "# You can also use ? or help() in Jupyter to see the docs\n", + "ts.spot.get_timeseries?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a3bffee", + "metadata": {}, + "outputs": [], + "source": [ + "# Example 2: Options data - pass parameters explicitly to each call\n", + "ts = TimeseriesDataManager(\"AAPL\")\n", + "\n", + "# Pass strike, expiration, right as parameters\n", + "vol_rt = ts.vol.rt(\n", + " strike=150.0,\n", + " expiration=\"2025-06-20\",\n", + " right=\"call\"\n", + ")\n", + "\n", + "greeks_series = ts.greeks.get_timeseries(\n", + " start_date=\"2025-01-01\",\n", + " end_date=\"2025-01-31\",\n", + " strike=150.0,\n", + " expiration=\"2025-06-20\",\n", + " right=\"call\"\n", + ")\n", + "\n", + "print(\"Vol RT:\", vol_rt)\n", + "print(\"Greeks series:\", greeks_series)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e622d9ff", + "metadata": {}, + "outputs": [], + "source": [ + "# Example 3: You can still access the underlying manager directly if needed\n", + "ts = TimeseriesDataManager(\"AAPL\")\n", + "\n", + "# Through adapter (standardized)\n", + "spot_result = ts.spot.get_timeseries(start_date=\"2025-01-01\", end_date=\"2025-01-31\")\n", + "\n", + "# Direct access to underlying manager (if you need specific methods)\n", + "# The adapter passes through any attribute not in the standard interface\n", + "underlying_manager = ts.spot._manager\n", + "print(f\"Underlying manager type: {type(underlying_manager)}\")\n", + "print(f\"Symbol: {ts.spot.symbol}\") # Passed through to underlying manager" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c6b545c5", + "metadata": {}, + "outputs": [], + "source": [ + "class TimeseriesDataManager:\n", + " def __init__(self, symbol: str, strike: float = None, expiration: DATE_HINT = None, right: str = None):\n", + " self._spot = SpotDataManager(symbol=symbol)\n", + " self._vol = VolDataManager(symbol=symbol)\n", + " self._dividend = DividendDataManager(symbol=symbol)\n", + " self._forward = ForwardDataManager(symbol=symbol)\n", + " self._option_spot = OptionSpotDataManager(symbol=symbol)\n", + " self._greeks = GreekDataManager(symbol=symbol)\n", + " self._rates = RatesDataManager()\n", + " self.strike = strike\n", + " self.expiration = expiration\n", + " self.right = right" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/notebooks/timeseries_fix.ipynb b/trade/datamanager/notebooks/timeseries_fix.ipynb new file mode 100644 index 0000000..79d3914 --- /dev/null +++ b/trade/datamanager/notebooks/timeseries_fix.ipynb @@ -0,0 +1,1183 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "a7c5bfcb", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:00 trade.helpers.Logging INFO: Logging Root Directory: /Users/chiemelienwanisobi/cloned_repos/QuantTools/logs\n", + "2026-02-02 20:26:00 [test] trade.helpers.clear_cache INFO: No expired caches to delete on 2026-02-02.\n", + "2026-02-02 20:26:02 [test] dbase.DataAPI.ThetaData.proxy INFO: Refreshed proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-02 20:26:02 [test] dbase.DataAPI.ThetaData.proxy INFO: Using Proxy URL: http://54.205.248.219:5500/thetadata\n", + "2026-02-02 20:26:02 [test] dbase.DataAPI.ThetaData INFO: Using V2 of the ThetaData API\n", + "\n", + "\n", + "Scheduled Data Requests will be saved to: /Users/chiemelienwanisobi/cloned_repos/QuantTools/module_test/raw_code/DataManagers/scheduler/requests.jsonl\n", + "2026-02-02 20:26:05 [test] DataManager.py CRITICAL: Using ProcessSaveManager for saving data.\n", + "Fetching rates data from yfinance directly during market hours\n", + "YF.download() has changed argument auto_adjust default to True\n" + ] + } + ], + "source": [ + "from trade.datamanager.market_data import MarketTimeseries\n", + "from trade.datamanager.utils.cache import _data_structure_cache_check_missing\n", + "from trade.optionlib.assets.dividend import get_div_histories, infer_frequency, FREQ_MAP, get_div_schedule" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "6153becb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fetching rates data from yfinance directly during market hours\n" + ] + } + ], + "source": [ + "ts = MarketTimeseries()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "2c0229f3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:10 [test] trade.datamanager.utils INFO: Cutting off today's data for key: C to avoid saving partial day data.\n", + "2026-02-02 20:26:10 [test] trade.datamanager.market_data INFO: Loaded chain spot data for symbol C into cache.\n", + "2026-02-02 20:26:10 [test] trade.datamanager.market_data INFO: Loaded split factor data for symbol C into cache.\n" + ] + } + ], + "source": [ + "ts._load_split_factor_into_cache(\"C\", start=\"2020-01-01\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ab836f1b", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[autoreload of trade.datamanager.rates failed: Traceback (most recent call last):\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 276, in check\n", + " superreload(m, reload, self.old_objects)\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 475, in superreload\n", + " module = reload(module)\n", + " ^^^^^^^^^^^^^^\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/importlib/__init__.py\", line 169, in reload\n", + " _bootstrap._exec(spec, module)\n", + " File \"\", line 621, in _exec\n", + " File \"\", line 936, in exec_module\n", + " File \"\", line 1074, in get_code\n", + " File \"\", line 1004, in source_to_code\n", + " File \"\", line 241, in _call_with_frames_removed\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/rates.py\", line 515\n", + " data_min = yf_ticker.history(\n", + " ^^^^^^^^\n", + "IndentationError: expected an indented block after 'try' statement on line 514\n", + "]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openhighlowclosevolumechain_priceunadjusted_closesplit_ratiocum_splitsplit_factormax_cum_splitis_split_date
Date
2005-01-03303.826962303.826962298.866522299.3005681474660299.300568299.3005681.01.01.01.0False
2005-01-04299.362613302.400898295.766300296.7583921584770296.758392296.7583921.01.01.01.0False
2005-01-05297.936409302.462802297.750400300.4786381893350300.478638300.4786381.01.01.01.0False
2005-01-06302.586728305.314965302.214691303.3927921804990303.392792303.3927921.01.01.01.0False
2005-01-07304.446955304.508964301.346680301.6567081280370301.656708301.6567081.01.01.01.0False
.......................................
2026-01-26113.900002115.480003113.860001114.82000011728700114.820000114.8200001.01.01.01.0False
2026-01-27114.900002115.970001113.699997114.79000111976800114.790001114.7900011.01.01.01.0False
2026-01-28114.599998115.709999113.139999114.19999711378300114.199997114.1999971.01.01.01.0False
2026-01-29114.839996116.360001113.400002115.19999714443200115.199997115.1999971.01.01.01.0False
2026-01-30114.419998116.650002114.180000115.70999912391800115.709999115.7099991.01.01.01.0False
\n", + "

5303 rows × 12 columns

\n", + "
" + ], + "text/plain": [ + " open high low close volume \\\n", + "Date \n", + "2005-01-03 303.826962 303.826962 298.866522 299.300568 1474660 \n", + "2005-01-04 299.362613 302.400898 295.766300 296.758392 1584770 \n", + "2005-01-05 297.936409 302.462802 297.750400 300.478638 1893350 \n", + "2005-01-06 302.586728 305.314965 302.214691 303.392792 1804990 \n", + "2005-01-07 304.446955 304.508964 301.346680 301.656708 1280370 \n", + "... ... ... ... ... ... \n", + "2026-01-26 113.900002 115.480003 113.860001 114.820000 11728700 \n", + "2026-01-27 114.900002 115.970001 113.699997 114.790001 11976800 \n", + "2026-01-28 114.599998 115.709999 113.139999 114.199997 11378300 \n", + "2026-01-29 114.839996 116.360001 113.400002 115.199997 14443200 \n", + "2026-01-30 114.419998 116.650002 114.180000 115.709999 12391800 \n", + "\n", + " chain_price unadjusted_close split_ratio cum_split \\\n", + "Date \n", + "2005-01-03 299.300568 299.300568 1.0 1.0 \n", + "2005-01-04 296.758392 296.758392 1.0 1.0 \n", + "2005-01-05 300.478638 300.478638 1.0 1.0 \n", + "2005-01-06 303.392792 303.392792 1.0 1.0 \n", + "2005-01-07 301.656708 301.656708 1.0 1.0 \n", + "... ... ... ... ... \n", + "2026-01-26 114.820000 114.820000 1.0 1.0 \n", + "2026-01-27 114.790001 114.790001 1.0 1.0 \n", + "2026-01-28 114.199997 114.199997 1.0 1.0 \n", + "2026-01-29 115.199997 115.199997 1.0 1.0 \n", + "2026-01-30 115.709999 115.709999 1.0 1.0 \n", + "\n", + " split_factor max_cum_split is_split_date \n", + "Date \n", + "2005-01-03 1.0 1.0 False \n", + "2005-01-04 1.0 1.0 False \n", + "2005-01-05 1.0 1.0 False \n", + "2005-01-06 1.0 1.0 False \n", + "2005-01-07 1.0 1.0 False \n", + "... ... ... ... \n", + "2026-01-26 1.0 1.0 False \n", + "2026-01-27 1.0 1.0 False \n", + "2026-01-28 1.0 1.0 False \n", + "2026-01-29 1.0 1.0 False \n", + "2026-01-30 1.0 1.0 False \n", + "\n", + "[5303 rows x 12 columns]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts._spot[\"C\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9e449783", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:11 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2024-06-20...\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
openhighlowclosevolumechain_priceunadjusted_closesplit_ratiocum_splitsplit_factormax_cum_splitis_split_date
datetime
2005-01-03303.827024303.827024298.866582299.3006291474660299.300629299.3006291.01.01.01.0False
2005-01-04299.362520302.400805295.766209296.7583011584770296.758301296.7583011.01.01.01.0False
2005-01-05297.936378302.462771297.750369300.4786071893350300.478607300.4786071.01.01.01.0False
2005-01-06302.586880305.315119302.214843303.3929441804990303.392944303.3929441.01.01.01.0False
2005-01-07304.447047304.509056301.346772301.6567991280370301.656799301.6567991.01.01.01.0False
.......................................
2024-06-1357.07686457.66912556.50370957.535389935820057.53538957.5353891.01.01.01.0False
2024-06-1456.78073957.15329456.03563656.6756631051100056.67566356.6756631.01.01.01.0False
2024-06-1756.57058157.82197156.21713257.3730011552750057.37300157.3730011.01.01.01.0False
2024-06-1857.66913058.87276257.40165858.0607871576810058.06078758.0607871.01.01.01.0False
2024-06-2057.71689158.29005057.65002357.907944951920057.90794457.9079441.01.01.01.0False
\n", + "

4899 rows × 12 columns

\n", + "
" + ], + "text/plain": [ + " open high low close volume \\\n", + "datetime \n", + "2005-01-03 303.827024 303.827024 298.866582 299.300629 1474660 \n", + "2005-01-04 299.362520 302.400805 295.766209 296.758301 1584770 \n", + "2005-01-05 297.936378 302.462771 297.750369 300.478607 1893350 \n", + "2005-01-06 302.586880 305.315119 302.214843 303.392944 1804990 \n", + "2005-01-07 304.447047 304.509056 301.346772 301.656799 1280370 \n", + "... ... ... ... ... ... \n", + "2024-06-13 57.076864 57.669125 56.503709 57.535389 9358200 \n", + "2024-06-14 56.780739 57.153294 56.035636 56.675663 10511000 \n", + "2024-06-17 56.570581 57.821971 56.217132 57.373001 15527500 \n", + "2024-06-18 57.669130 58.872762 57.401658 58.060787 15768100 \n", + "2024-06-20 57.716891 58.290050 57.650023 57.907944 9519200 \n", + "\n", + " chain_price unadjusted_close split_ratio cum_split \\\n", + "datetime \n", + "2005-01-03 299.300629 299.300629 1.0 1.0 \n", + "2005-01-04 296.758301 296.758301 1.0 1.0 \n", + "2005-01-05 300.478607 300.478607 1.0 1.0 \n", + "2005-01-06 303.392944 303.392944 1.0 1.0 \n", + "2005-01-07 301.656799 301.656799 1.0 1.0 \n", + "... ... ... ... ... \n", + "2024-06-13 57.535389 57.535389 1.0 1.0 \n", + "2024-06-14 56.675663 56.675663 1.0 1.0 \n", + "2024-06-17 57.373001 57.373001 1.0 1.0 \n", + "2024-06-18 58.060787 58.060787 1.0 1.0 \n", + "2024-06-20 57.907944 57.907944 1.0 1.0 \n", + "\n", + " split_factor max_cum_split is_split_date \n", + "datetime \n", + "2005-01-03 1.0 1.0 False \n", + "2005-01-04 1.0 1.0 False \n", + "2005-01-05 1.0 1.0 False \n", + "2005-01-06 1.0 1.0 False \n", + "2005-01-07 1.0 1.0 False \n", + "... ... ... ... \n", + "2024-06-13 1.0 1.0 False \n", + "2024-06-14 1.0 1.0 False \n", + "2024-06-17 1.0 1.0 False \n", + "2024-06-18 1.0 1.0 False \n", + "2024-06-20 1.0 1.0 False \n", + "\n", + "[4899 rows x 12 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts._get_chain_spot_timeseries(\"C\", start=\"2005-01-01\", end=\"2024-06-20\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "8ce0375c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:11 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:11 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2024-06-20...\n", + "2026-02-02 20:26:12 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:12 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2024-06-20...\n", + "2026-02-02 20:26:12 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:12 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2026-01-30...\n" + ] + } + ], + "source": [ + "t = ts.get_timeseries(\"C\", start_date=\"2005-01-01\", end_date=\"2024-06-20\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3bd799f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "datetime\n", + "2005-01-03 1.0\n", + "2005-01-04 1.0\n", + "2005-01-05 1.0\n", + "2005-01-06 1.0\n", + "2005-01-07 1.0\n", + " ... \n", + "2026-01-26 1.0\n", + "2026-01-27 1.0\n", + "2026-01-28 1.0\n", + "2026-01-29 1.0\n", + "2026-01-30 1.0\n", + "Name: split_factor, Length: 5303, dtype: float64" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts._get_split_factor_timeseries(\"C\", start=\"2005-01-01\", end=\"2024-06-20\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c38e1205", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2024-06-20 00:00:00...\n", + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2024-06-20 00:00:00...\n", + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:13 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2026-01-30...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[autoreload of trade.datamanager.rates failed: Traceback (most recent call last):\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 276, in check\n", + " superreload(m, reload, self.old_objects)\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 475, in superreload\n", + " module = reload(module)\n", + " ^^^^^^^^^^^^^^\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/importlib/__init__.py\", line 169, in reload\n", + " _bootstrap._exec(spec, module)\n", + " File \"\", line 621, in _exec\n", + " File \"\", line 936, in exec_module\n", + " File \"\", line 1074, in get_code\n", + " File \"\", line 1004, in source_to_code\n", + " File \"\", line 241, in _call_with_frames_removed\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/rates.py\", line 524\n", + " data_min.columns = data_min.columns.str.lower()\n", + " ^^^^^^^^\n", + "SyntaxError: expected 'except' or 'finally' block\n", + "]\n" + ] + }, + { + "data": { + "text/plain": [ + "AtIndexResult(sym=C, date=2024-06-20)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts.get_at_index(\"C\", index=\"2024-06-20\")" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "7e21a7eb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2024-06-20 00:00:00...\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2024-06-20 00:00:00...\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2024-06-20 00:00:00...\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 22:21:49 [test] trade.datamanager.utils INFO: Sanitizing data from 2024-06-20 00:00:00 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "np.float64(0.009152458208936249)" + ] + }, + "execution_count": 64, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts.get_at_index(\"C\", index=\"2024-06-20\", ).dividend_yield" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4f156cca", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[*********************100%***********************] 1 of 1 completed\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAGZCAYAAACjc8rIAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAS/BJREFUeJzt3Xd8FHX+P/DXZ3Y3nWQTkhAglBRCkC5NRTQqdixYwPP0UMrpoQd3noJHOYWvyKECd6jnDxVUrIcIggIqYkHEA8QCSAm9JiSBFFK3zOf3x2YnWdJhMzubfT0fDx6Z+czs7HveCZt3PvOZzwgppQQRERGRgSi+DoCIiIjoXCxQiIiIyHBYoBAREZHhsEAhIiIiw2GBQkRERIbDAoWIiIgMhwUKERERGQ4LFCIiIjIcFihERERkOCxQiIiIyHDMTX3Brl27sGrVKhw6dAj5+fl4/PHHMXDgwFr3ffXVV/Hll19i1KhRuPnmm7X24uJiLF68GNu2bYMQAoMGDcKDDz6IkJCQJsWSn58Ph8PR1FOg8xAXF4fc3FxfhxFQmHN9Md/6Y8715+ucm81mREdHN27fph68oqICnTt3xtVXX40XXnihzv22bNmCffv21RrIggULkJ+fj2nTpsHpdOI///kPFi5ciIkTJzYpFofDAbvd3tRToCYSQgBw5ZuPbtIHc64v5lt/zLn+/C3nTb7E07dvX9xzzz119poAwJkzZ7B48WJMmDABZrNnDXT8+HH88ssvePjhh9GlSxekp6dj9OjR2LRpE86cOdP0MyAiIqIWp8k9KA1RVRUvvvgibr31VnTo0KHG9szMTISHhyMlJUVr69mzJ4QQ2L9/f62Fj91u9+gpEUIgNDRUW6bm5c4xc60f5lxfzLf+mHP9+VvOvV6grFy5EiaTCTfeeGOt2wsKChAZGenRZjKZEBERgYKCglpfs2LFCixbtkxbT0pKwpw5cxAXF+e1uKlhCQkJvg4h4DDn+mK+9cec689fcu7VAuXgwYNYs2YN5syZ49UKbfjw4Rg2bJi27j52bm4uB8nqQAiBhIQEZGdn+8V1y5aAOdcX860/5lx/Rsi52WxudOeCVwuU3bt3o6ioCOPHj9faVFXFkiVLsGbNGrz88suwWq0oKiryeJ3T6URxcTGsVmutx7VYLLBYLLVuqyvJFRUVqKioOL8ToRpKS0ths9kQHByM4OBgX4cTMKSU/PDWEfOtP+Zcf/6Sc68WKFdccQV69uzp0TZr1ixcccUVuOqqqwAAaWlpKCkpwcGDB5GcnAwA2LlzJ6SUSE1N9UocJSUlEEKgVatWfnOtzegsFgtsNhvKyspQUlKC8PBwX4dEREQtWJMLlPLycmRnZ2vrOTk5OHz4MCIiIhAbG4tWrVp5voHZDKvVinbt2gEAEhMT0adPHyxcuBDjxo2Dw+HA4sWLcdlllyEmJuYCT8fF4XAgKirKK8eiKkIIhIWFobCw0NehEBFRC9fkAuXAgQOYMWOGtr5kyRIAwJVXXolHHnmkUceYMGECFi1ahJkzZ2oTtY0ePbqpodSJvSbNi/klIqLmJqQ/XIiqQ25ubq0TtRUVFdW4U4gujMVi0XLN/DY/IQTatm2LrKwsv7hW7O+Yb/0x5/ozQs4tFkujB8nyWTxERERkOCxQ/Ez79u3x2Wef+ToMIiJqoeRPP0BdNA/q91/6NA4WKAaTk5ODadOm4dJLL0VSUhL69++PUaNG4bvvvvN1aEREFADk8cOQ//sGOJjp0zi8PpMsnb9jx47h9ttvR2RkJKZNm4b09HQ4HA588803mDp1KjZs2ODrEImIqMWrHJ+i+PaGiIAoUKSUgM0Hk7YFBTfpjpcpU6YAAFavXo2wsDCtvWvXrrjnnntqfc3u3bvxj3/8Az/99BNCQkJw880346mnntLmKdm0aRNmzZqFvXv3wmKxIC0tDS+//DISExMBAJ9//jnmzZuHffv2oU2bNrj77rtrfcgjEREFCG0ALQuU5mergProCN3fVnlpKRAc0qh98/Pz8fXXX2Py5MkexYlbbfO6lJaW4ve//z369euH1atXIy8vD0888QSmTp2Kf/3rX3A4HBgzZgzuvfdevPzyy7Db7fj555+1omnz5s2YOHEiZs6ciUGDBuHIkSOYNGkSAOCxxx67gDMnIiK/5S5QfDylRGAUKH7g8OHDTZ5Nd8WKFaioqMC///1vrah55pln8MADD2Dq1Kkwm80oKirC0KFD0blzZwBAly5dtNfPmzcPjzzyCEaMcBVvnTp1whNPPIFZs2axQCEiClTuAkXx7TDVwChQgoJdvRk+eN/GOp970vft24du3bp59LgMGDAAqqriwIEDuOSSSzBixAj8/ve/x5AhQzBkyBDccsstaNOmDQBg165d+PHHH7FgwQLt9aqqory8HGVlZQgNDW1yTERE5Oek6usIAARIgSKEaPSlFl9JSkqCEAL79+/36nHnz5+PMWPG4Ouvv8aqVavw3HPP4f3330e/fv1QWlqKv/3tb7jxxhtrvI4PBCQiClDaEBTf9qDwNmODiI6ORkZGBt58802UlpbW2F7b82+6dOmC3bt3e+y/detWKIqClJQUra1Hjx7485//jFWrVqFr1674+OOPtfYDBw4gKSmpxj/Fx117RETkGzL7eOWCb3tS+FvIQGbNmgVVVXHzzTdj9erVOHjwIPbt24dFixbh1ltvrbH/HXfcgeDgYEycOBF79uzB999/j+nTp+POO+9EXFwcjh49itmzZ+PHH3/E8ePH8e233+LQoUPaOJe//vWvWLZsGebNm4e9e/di3759WLlyJebMmaP3qRMRkVH8shkAINd/4tMwAuISj7/o1KkTPvvsMyxYsAAzZ85ETk4OYmJi0KtXL8yePbvG/qGhoXj33Xfxj3/8AzfffLPHbcbu7fv378eHH36I/Px8xMfH44EHHsD9998PAMjIyMBbb72F+fPn4+WXX4bFYkFqaip+97vf6XreRERE5+LDAqlR+LBAfRnhoV6BhPnWH3Ouv8bm3Dmuqsfe9Noqr8bAhwUSERHR+YlvBwAQf3jUp2GwQCEiIqIqEa0AAKJVzQlC9cQChYiIiKpoM8nyNmMiIiIyCrXy9mIfPyyQBQoRERFVMcizeFpsgaKqxpiqt6VhXomIWjj3BG28xON9YWFhOHv2LH+Zepmqqjh79mytT1smIqIWwiA9KC1yojaz2Yzw8HAUFxf7OpQWIzg4GBUVFQgPD4fZ3CJ/bIiICGCB0tzMZjMnE/MSTqhERBRAtEGyvMRDRERERmGQHhQWKERERFSF86AQERGR4Wh38bAHhYiIiIyCl3iIiIjIcDhIloiIiAyHPShERERkOBwkS0RERIbDQbJERERkJLKkGCg441rh04yJiIjICOSapVUrvMRDREREhnC2qGq5rMR3ceA8nsWza9curFq1CocOHUJ+fj4ef/xxDBw4EADgcDjwwQcf4Oeff0ZOTg7CwsLQs2dP3HvvvYiJidGOUVxcjMWLF2Pbtm0QQmDQoEF48MEHERIS4r0zIyIioiaRpdUesltW5rtAcB49KBUVFejcuTPGjBlTY5vNZsOhQ4dw5513Ys6cOfjb3/6GkydP4rnnnvPYb8GCBTh27BimTZuGJ598Ert378bChQvP/yyIiIjogonQ8KoVa0zdO+qgyT0offv2Rd++fWvdFhYWhunTp3u0jR49GlOmTEFeXh5iY2Nx/Phx/PLLL5g9ezZSUlK0fWbPno3777/fo6fFzW63w263a+tCCISGhmrL1LzcOWau9cOc64v51h9zrr/G5Fz0GgD5v68BAErHZF3iqkuTC5SmKi0thRACYWFhAIDMzEyEh4drxQkA9OzZE0II7N+/X7tcVN2KFSuwbNkybT0pKQlz5sxBXFxcc4dP1SQkJPg6hIDDnOuL+dYfc66/+nJeao3CaQDBvfojvm1b/YKqRbMWKDabDe+++y4GDx6sFSgFBQWIjIz02M9kMiEiIgIFBQW1Hmf48OEYNmyYtu6u/nJzc+FwOJoneNIIIZCQkIDs7GxI9wQ+1KyYc30x3/pjzvXXmJyr+a5bjG02G7Kysrweg9lsbnTnQrMVKA6HA/PnzwcAjB079oKOZbFYYLFYat3GH2z9SCmZb50x5/pivvXHnOuvvpxL1dUuhfD596VZbjN2Fyd5eXmYNm2a1nsCAFarFUVFRR77O51OFBcXw2q1Nkc4RERE1CjGeA4P0AwFirs4yc7OxvTp09GqVSuP7WlpaSgpKcHBgwe1tp07d0JKidTUVG+HQ0RERI2ldZr4vkBp8iWe8vJyZGdna+s5OTk4fPgwIiIiYLVaMW/ePBw6dAiTJ0+GqqrauJKIiAiYzWYkJiaiT58+WLhwIcaNGweHw4HFixfjsssuq/UOHiIiItKJ9qBA34YBnEeBcuDAAcyYMUNbX7JkCQDgyiuvxN13340ff/wRADBp0iSP1z311FPo3r07AGDChAlYtGgRZs6cqU3UNnr06PM+CSIiIvICgzzJGDiPAqV79+5YunRpndvr2+YWERGBiRMnNvWtiYiIqFm14DEoRERE5KcMdEcVCxQiIiJycTpdX03NPo9rg1igEBERkYvTNfmpMJl8HAgLFCIiIqok33/V9fXkUR9HwgKFiIiIzpV1zNcRsEAhIiIi42GBQkRERIbDAoWIiIgMhwUKERERGQ4LFCIiIjIcFihERERkOCxQiIiIyHBYoBAREZHhsEAhIiIiw2GBQkRERIbDAoWIiIg8iIybfB0CCxQiIiLyJG6809chsEAhIiIiQEpZtWI2+y6QSixQiIiICFDVqmUTCxQiIiIyAqejalkx+S4Odwi+DoCIiIgMQHVWLZtYoBAREZEROKsVKOxBISIiIkOoPgZF8X154PsIiIiIyPdkVYEiWKAQERGRIciGd9ETCxQiIiIC3POgCOHbOCqxQCEiIiJoXSgsUIiIiMgwVBYoREREZDjuQSgsUIiIiMgotPqEBQoREREZBi/xEBERkdG4J2ozRn3CAoWIiIiqEcYoDZr8POVdu3Zh1apVOHToEPLz8/H4449j4MCB2nYpJZYuXYr169ejpKQE6enpGDt2LNq2bavtU1xcjMWLF2Pbtm0QQmDQoEF48MEHERIS4p2zIiIioqaRfj5ItqKiAp07d8aYMWNq3b5y5UqsXbsW48aNw7PPPovg4GDMmjULNptN22fBggU4duwYpk2bhieffBK7d+/GwoULz/8siIiI6AK5x6D4Ngq3Jveg9O3bF3379q11m5QSa9aswR133IEBAwYAAB599FGMGzcOW7duxeDBg3H8+HH88ssvmD17NlJSUgAAo0ePxuzZs3H//fcjJiamxnHtdjvsdru2LoRAaGiotkzNy51j5lo/zLm+mG/9Mef6azDnWgeKYojvS5MLlPrk5OSgoKAAvXr10trCwsKQmpqKzMxMDB48GJmZmQgPD9eKEwDo2bMnhBDYv3+/x+UitxUrVmDZsmXaelJSEubMmYO4uDhvhk8NSEhI8HUIAYc51xfzrT/mXH915dzurEA2XA8KrD4sw1e8WqAUFBQAAKKiojzao6KitG0FBQWIjIz02G4ymRAREaHtc67hw4dj2LBh2rq7ssvNzYXD4fBO8FQnIQQSEhKQnZ0NKQ32NKkWijnXF/OtP+Zcfw3lXObkuL5KiaysrGaJwWw2N7pzwasFSnOxWCywWCy1buMPtn6klMy3zphzfTHf+mPO9VdXzqV2m7EwxPfEq/cSWa1WAEBhYaFHe2FhobbNarWiqKjIY7vT6URxcbG2DxEREemsJc8kGx8fD6vVih07dmhtpaWl2L9/P9LS0gAAaWlpKCkpwcGDB7V9du7cCSklUlNTvRkOERERNZqxZpJt8iWe8vJyZGdna+s5OTk4fPgwIiIiEBsbi5tuugnLly9H27ZtER8fjw8++ADR0dHaXT2JiYno06cPFi5ciHHjxsHhcGDx4sW47LLLar2Dh4iIiHRggMs61TW5QDlw4ABmzJihrS9ZsgQAcOWVV+KRRx7BbbfdhoqKCixcuBClpaVIT0/HlClTEBQUpL1mwoQJWLRoEWbOnKlN1DZ69GgvnA4RERGdF3eBovjpTLLdu3fH0qVL69wuhMDIkSMxcuTIOveJiIjAxIkTm/rWRERE1Fz8fSZZIiIiaomMNZMsCxQiIiKqeppxwRnfxlGJBQoREVGAUjd/C/WD1yBVFXLrd74Ox4NfTNRGRERE3idfnwsAEL36Q/6y2cfReGIPChERUQCSFRVVK6oEcppnevvzxQKFiIgoENnKq5ZDQn0XRx1YoBAREQUi96DYc5cNggUKERFRAJL/+6baCgsUIiIi8jFpq4Bc9kZVA3tQiIiIyOccdo9VubfqIb/i0qv1jqZWLFCIiIgCnDy4t2qlaw/fBVINCxQiIqJAZ6t2y7Fi8l0c1bBAISIiCjTynPUDe6qWzRZdQ6kLCxQiIqKAc26FUkWEhOgYR91YoBAREQUaWXeBgrgE/eKoBwsUIiKiQFNfgRJsjFllWaAQEREFmvoKlPq26YgFChERUaBRnXVvs8boF0c9WKAQEREFGmcdM8cmd4VQjFEaGCMKIiIi0o/TUXt79QnbfIwFChERUaBx1nOJxyBYoBAREQWa+sagGAQLFCIiokBT1yUeA2GBQkREFGh4iYeIiIgMhwUKERERGY67QElIhOh/uW9jqQMLFCIiokDjHoNiMgEpXX0bSx1YoBAREQUa9108JjMgjFkKGDMqIiIiaj7uSzwmEyCEb2OpAwsUIiKiQFP9Ek/1HpSwcN/EUwsWKERERAFGup/FYzIDsuq5PMr4KT6KqCYWKERERIHG3YOiKEDx2ar2oGDfxFMLFihERESBxlltkGx1ikn/WOpgbniXplFVFUuXLsV3332HgoICxMTE4Morr8Sdd94JUTkQR0qJpUuXYv369SgpKUF6ejrGjh2Ltm3bejscIiIiOpdabZBsdQYaL+v1HpSPP/4Y69atw5gxYzB//nz8/ve/x6pVq7B27Vptn5UrV2Lt2rUYN24cnn32WQQHB2PWrFmw2WzeDoeIiIjOVX2QrEF5vQclMzMT/fv3x8UXXwwAiI+Px8aNG7F//34Art6TNWvW4I477sCAAQMAAI8++ijGjRuHrVu3YvDgwTWOabfbYbfbtXUhBEJDQ7Vlal7uHDPX+mHO9cV8648511/1nAunExKAOOcSj4BxvideL1DS0tKwfv16nDx5Eu3atcPhw4exd+9e/OEPfwAA5OTkoKCgAL169dJeExYWhtTUVGRmZtZaoKxYsQLLli3T1pOSkjBnzhzExcV5O3yqR0JCgq9DCDjMub6Yb/0x5/pLSEjA2YhwFAAIjYiAOTwMRZXbYuPiEWSQ4RZeL1Buv/12lJWV4a9//SsURYGqqrjnnnswZMgQAEBBQQEAICoqyuN1UVFR2rZzDR8+HMOGDdPW3dVdbm4uHA7jPzLa3wkhkJCQgOzsbEgpfR1OQGDO9cV8648511/1nDs2fgUAKDuVBYRGaPvkhURAZGU1Wwxms7nRnQteL1B++OEHbNy4ERMmTECHDh1w+PBhvPnmm4iOjkZGRsZ5HdNiscBisdS6jT/Y+pFSMt86Y871xXzrjznXn5QScs921/KuXyA6JFdtFMIw3w+vFyjvvPMObrvtNu1STceOHZGbm4uPP/4YGRkZsFqtAIDCwkJER0drryssLETnzp29HQ4RERHVp9pEbUbi9bt4KioqoCieh1UURavI4uPjYbVasWPHDm17aWkp9u/fj7S0NG+HQ0RERPVRjVmgeL0HpV+/fli+fDliY2ORmJiIw4cP49NPP8VVV10FwHUN7KabbsLy5cvRtm1bxMfH44MPPkB0dLR2Vw8RERHpoH2nwClQRo8ejf/+9794/fXXUVhYiJiYGFx77bW46667tH1uu+02VFRUYOHChSgtLUV6ejqmTJmCoKAgb4dDRERE54qKAQrPQLn3IcgfN/o6mlp5vUAJDQ3FAw88gAceeKDOfYQQGDlyJEaOHOnttyciIqKGuCdoswQZtgeFz+IhIiIKOJV36ggBdEyuf1cf8XoPChERERmcWlWgiMuvBewOiK7dfRvTOVigEBERBRz3XCcCQjFBXDOs3r19gZd4iIiIAo1WnxjjuTu1YYFCREQUQKTdBhSeca2wQCEiIiIjUD9+t2rFuPUJCxQiIqJAIn/dXLUijFsGGDcyIiIi8j6PosS4XSgsUIiIiAJJ1rGqZePWJyxQiIiIAhYv8RAREZHhsAeFiIiIDMFcfY5W41YoLFCIiIgCSrWiRGGBQkREREbgsFdbYYFCRERERsOZZImIiIgajwUKERFRoJKy4X18hAUKERFRIKl+Wcfp8F0cDWCBQkREFFCqFSgOFihERETkY1JKQKpVDcHBvgumASxQiIiIAkW1HhNx410Q8e18GEz9WKAQEREFCFltDhQxbKQPI2kYCxQiIqIAIe22qhWPKe+NhwUKERFRoHBf4hEKhGLybSwNYIFCREQUILRLPAbvPQFYoBAREQUMaWeBQkREREbj7kExsUAhIiIig+AlHiIiIjIcaa8cJMseFCIiIjKKit9+di2czvFtII3AAoWIiChAFC7+t69DaDQWKERERGQ4zXIR6syZM3jnnXfwyy+/oKKiAgkJCRg/fjxSUlIAuB5WtHTpUqxfvx4lJSVIT0/H2LFj0bZt2+YIh4iIiPyM1wuU4uJiTJ8+Hd27d8eUKVMQGRmJrKwshIeHa/usXLkSa9euxSOPPIL4+Hj897//xaxZszBv3jwEBQV5OyQiIiLyM16/xLNy5Uq0bt0a48ePR2pqKuLj49G7d28kJCQAcPWerFmzBnfccQcGDBiATp064dFHH0V+fj62bt3q7XCIiIjITQjXl7se9HEgDfN6D8qPP/6I3r17Y968edi1axdiYmJw3XXXYejQoQCAnJwcFBQUoFevXtprwsLCkJqaiszMTAwePLjGMe12O+z2ak9gFAKhoaHaMjUvd46Za/0w5/pivvXHnOtPCAFLSjrs+3dDtOtg+Nx7vUDJycnBunXrcPPNN2P48OE4cOAA3njjDZjNZmRkZKCgoAAAEBUV5fG6qKgobdu5VqxYgWXLlmnrSUlJmDNnDuLi4rwdPtXD3QtG+mHO9cV8648511dWaTEAoHX7RIQYfNyn1wsUVVWRkpKCe++9F4CrmDh69CjWrVuHjIyM8zrm8OHDMWzYMG3dXfXl5ubC4X4yIzUbIQQSEhKQnZ0NKaWvwwkIzLm+mG/9Mef6E0JALXEVKGfKbRBZWbrHYDabG9254PUCJTo6GomJiR5tiYmJ2Lx5MwDAarUCAAoLCxEdHa3tU1hYiM6dO9d6TIvFAovFUus2/mDrR0rJfOuMOdcX860/5lxfsqLc9dUSBBg8714fJNu1a1ecPHnSo+3kyZNaxRQfHw+r1YodO3Zo20tLS7F//36kpaV5OxwiIiJCZTFYWaAgONi3wTSC1wuUm2++Gfv27cPy5cuRnZ2NjRs3Yv369bj++usBuLqYbrrpJixfvhw//vgjjh49ipdeegnR0dEYMGCAt8MhIiIiALDbqnpNgoxfoHj9Ek9qaioef/xxvPfee/joo48QHx+PUaNGYciQIdo+t912GyoqKrBw4UKUlpYiPT0dU6ZM4RwoREREzcVWUbUciAUKAPTr1w/9+vWrc7sQAiNHjsTIkSOb4+2JiIjoXO7LO2YzhGLybSyNwGfxEBERBQB5qnJ8qJ/c/coChYiIKADIzd/6OoQmYYFCREQUAESXi3wdQpOwQCEiIgoA8sAe10JqN98G0kgsUIiIiAKA/O4L18L+3b4NpJFYoBAREQUA0f1i19crrvdxJI3DAoWIiCgQRLQCAIiExAZ2NAYWKERERIHAbnN9tfjHpKgsUIiIiAKB3e76ygKFiIiIDMPhLlCaZRJ5r2OBQkREFACk+xKPmT0oREREZBS8xENERERGIaV0LWiXeCy+C6YJWKAQERG1UOrbL0Od+hDk0YNA3ikAgDD7R4HiHyNliIiIqMnkhs8BAOr//aWqkT0oREREpBd1/adQP1+urcudP9W+o5+MQWEPChERkZ+TdhvkB6+6li+9CiIyGuoXK2rf2U8u8bAHhYiIyN+5byEGgFNZAACRUsdTi3mJh4iIiHRhqypQ5IkjrgVTHb/i/eQSDwsUIiIiPyUzd0Jd9T5QUV7VqDpdX8vLa3+RnxQoHINCRETkp9TnpwAA5I8bqxptFZA/bQLO5Nb+ouAQHSK7cCxQiIiI/F3WMW1RfvkJZOGZWndr9+4XyCmrqJq8zcB4iYeIiKglqaM4AQCTNUbHQC4MCxQiIiI/JM8WNml/ccX1zRRJ82CBQkRE5I9OHm3a/lHRzRNHM2GBQkRE5I9aRTVpd+Xmkc0USPPgIFkiIiJ/pKqN2k2ZswgiJg5CiGYOyLvYg0JERORnZMFpyLXLGrez2T/7IvwzaiIiogCm/utpwD1jbENM/vmrnj0oRERE/qaxxQkAmEzNF0czYoFCRETUkpn84+GA52KBQkRE1JKxB4WIiIiam2zk3TtuQvHPX/XNHvXHH3+MESNG4M0339TabDYbXn/9dYwePRr3338/XnjhBRQUFDR3KERERP7P/bTiSqL/5T4KpHk1a4Gyf/9+rFu3Dp06dfJof+utt7Bt2zY89thjmDFjBvLz8zF37tzmDIWIiKhlcJ7Tg6L45yWchjRbgVJeXo4XX3wRDz30EMLDw7X20tJSfPXVVxg1ahR69OiB5ORkjB8/Hnv37kVmZmZzhUNERNQyHDvgsSrufgBoHQ9x671Q/vFviBvvBIKCfBObFzXbzdGvv/46+vbti169emH58uVa+8GDB+F0OtGzZ0+trX379oiNjUVmZibS0tJqHMtut8Nut2vrQgiEhoZqy9S83DlmrvXDnOuL+dYfc37+nHOe1JZN0+ZBRMdCmbOoaoeOyVC7dIe6YCbEsJE1cu0vOW+WAuX777/HoUOHMHv27BrbCgoKYDabPXpVACAqKqrOcSgrVqzAsmVVM+YlJSVhzpw5iIuL82rcVL+EhARfhxBwmHN9Md/6Y86bRtrtOF5tve0lQ2ovONreCvXyq6GER9TY5C8593qBkpeXhzfffBPTpk1DkJe6mIYPH45hw4Zp6+5vRm5uLhwOh1feg+omhEBCQgKys7MhpfR1OAGBOdcX860/5vz8yNM5HuvZ2dn1v6DorLZohJybzeZGdy54vUA5ePAgCgsLMXnyZK1NVVXs3r0bn332GaZOnQqHw4GSkhKPXpTCwkJYrdZaj2mxWGCx1D7RDH+w9SOlZL51xpzri/nWH3PeNLK0xHP9PHLnLzn3eoHSs2dPvPDCCx5tr7zyCtq1a4fbbrsNsbGxMJlM2LFjBy655BIAwMmTJ5GXl1fr+BMiIiKq5AeFhbd4vUAJDQ1Fx44dPdqCg4PRqlUrrf3qq6/GkiVLEBERgbCwMCxevBhpaWksUIiIiOojmzZJmz/zySMOR40aBSEE5s6dC4fDgd69e2Ps2LG+CIWIiMh/VO9B6ZDkuzh0oEuB8vTTT3usBwUFYezYsSxKiIiImqJagaL8bZYPA2l+/jlBPxERUSByFyit4yFquYW4JWGBQkRE5C/cDwr00wcANkXLP0MiIqKWwt2D4iezwV4IFihERET+QhuDwgKFiIiIjMJdoCgsUIiIiMgotEs8Lf/Xd8s/QyIiogskT52EuvJdyJKzDe/crIFUDpINgDEoPpmojYiIyF9Iuw3qtIddK6dOQvzxCR8Gw0GyREREBED+8FXV8tbvfBgJAqoHhQUKERFRfSoqfB1BFe0mHhYoREREge2cGVul0+mjQAD53ReuhazjPotBLyxQiIiI6iFCQj0bjuz3TSAA5LbvXQsOu89i0AsLFCIionqoC5/zXJ/9BOSJIz6KJnCwQCEiIqqDVJ1Vz7+p3u6+1ELNhgUKERFRXYoKa22W6z/ROZBKSWkAAHHDnb55fx2xQCEiIqrLqZO+jkAjpQQOZQIAROpFPo6m+bFAISIiqoupcb8m5ZlcyLxTzRqKOmNC1UpQULO+lxFwJlkiIqK6qLL29k6p2qJ0OqFOHgMAUF7+ECIouHliqT4wNyaued7DQNiDQkREVBf31PJtO3i2H9kPWTmBm/zph6r2Yu8/q0ce2gfn4w94NrJAISIiCmD1TC0vN3/t+vr58qrGZniYoPrs34DCMx5twmLx+vsYDQsUIiKiuqhVBYry3BtATGy1bRJSSojLh1Y1rXjbq28vz+TWaBO/f9ir72FULFCIiIjq4r7EoygQ0a2hzH69atO7r0D9422eDxDc8aN33/6Hr2u0icHXevU9jIoFChERUV3sNtdXk+ueEqHU8msz8zdtUVxxg3ffP9JaoykQLu8ALFCIiIjqJM9WTtQW0UprE9fcUvcLwsK9G0BRgceqGHild49vYLzNmIiIqC5HDwIAREK1u3hi29S9v7cf4pftemqxuPVeiGEjIWoZrNtSsQeFiIioDtrTg6OsVY31TZLm5QJF2ly3MqNVZEAVJwALFCIiorpVXuKRWzZUtZnrKVDsXu5Bcc+x0lyTvxkYCxQiIqIGiMur7pwROvWgqK/PrXrP4BCvHddfsEAhIiKqS7uOAABRfSZZS929GdJLBYpUVcjN31Y1BLFAISIiClhSSqgr34Pc+ZOr4ZzbjAHUeuuvxuHwTiC//ey5HoCXeHgXDxERUSX50ZuQn6+ABICoaKAw37Wh+p07HZPrPkD1B/pdAHXBDM+GhPZeOa4/YQ8KERFRJfn5iqoVd3ECANGttUVhMtV9gLxTkOf2flwg5YnZEFHRXj2mP2CBQkRE1ICm3OKrLnvjgt5LHt7n2ZDU5YKO56+8folnxYoV2LJlC06cOIGgoCCkpaXhvvvuQ7t27bR9bDYblixZgk2bNsFut6N3794YO3YsrFart8MhIiK6IGLobTXalKcWALnZkL9shty03nOjNeaC3k/96K1zAgis+U/cvN6DsmvXLlx//fWYNWsWpk2bBqfTiWeeeQbl5eXaPm+99Ra2bduGxx57DDNmzEB+fj7mzp1bz1GJiIiah7p0EZzjboW6fEntO1SU1WgSiZ0h+l4CMfw+V0ObqjEiokM9Y1QaQZw73qS25/8EAK+f9dSpU5GRkYEOHTqgc+fOeOSRR5CXl4eDB13TBZeWluKrr77CqFGj0KNHDyQnJ2P8+PHYu3cvMjMzvR0OERFRneTpHMh1K13La5fVuo/oNaDO1wtra5heWwXTM69AXD1MO468gMGy8pu1VSsJiYAIzAKl2e/iKS0tBQBEREQAAA4ePAin04mePXtq+7Rv3x6xsbHIzMxEWlpajWPY7XbYq83OJ4RAaGiotkzNy51j5lo/zLm+mG/9GSXnctcvtbaLm0dArl7qWu4zqHFxnjqhLaqv/BPKXaOg9L30guIzTXm+9iconwej5LyxmrVAUVUVb775Jrp27YqOHV2T3RQUFMBsNiM83POJj1FRUSgoKKj1OCtWrMCyZVWVbVJSEubMmYO4uLhmi51qSkhI8HUIAYc51xfzrT9f5/zMqeMoqaW9ze2/Q/bqpYAlyGMMZX2OVb9759QJqC8/i9h5byK4aw+U/vANyrd+D+uYiVDCI+o/TrXldineHyDr65w3VrMWKIsWLcKxY8cwc+bMCzrO8OHDMWzYMG3dXf3l5ubC4a1JcahOQggkJCQgOzsbUkpfhxMQmHN9Md/6M0rOHZ9/XGt7njDDNOMloFUUsrKyGnUscfm1kBvXebTlvvc6TA9PhuOZxwEAJZ+vgPn1T+o8hkcuYmIb/d6Nis8AOTebzY3uXGi2AmXRokX46aefMGPGDLRuXXX/uNVqhcPhQElJiUcvSmFhYZ138VgsFlgsllq38cNEP1JK5ltnzLm+mG/9+TLnsqy0ZmOnVIh+l7liqpzmvtHxte9U8z1+3AjV/phHm5qfB2FtXXPfs4VAtWfuKI8/2yy58Zefc6+PvJFSYtGiRdiyZQv+8Y9/ID4+3mN7cnIyTCYTduzYobWdPHkSeXl5tY4/ISIiahYH99ZoMk2bB+XGu87rcGLgkJqNF/WB+sxfPduOHKyxm/r5cqiP3Q+5vlrvSuvAHsbg9R6URYsWYePGjZg0aRJCQ0O1cSVhYWEICgpCWFgYrr76aixZsgQREREICwvD4sWLkZaWxgKFiIh0o/7rKW1ZXHoV0KPfBR1PREYD8e2AnJNVbeGtagzEVV/6P5heW+XRJpe96fpa7VZnodQzY20A8HqB8sUXXwAAnn76aY/28ePHIyMjAwAwatQoCCEwd+5cOBwObaI2IiIi3UVEQhn914b3a4xqxQkAyK3fNfgSeeSAd967hfF6gbJ06dIG9wkKCsLYsWNZlBARkU9I91OKAShPv+i9AweH1jqxm4ekqqsF8lAm1Gcf9977tyCBOfsLEREFHCklnK/8E+p7/w/Y95urUQigVZTX3kPceGfD+3RK0ZbrLE5S0r0Vkt9igUJERC2WVFVIVXWtnDgM/LQJ8us1UFd/WLmD9NpEaAAgrri+7m233ut6y/zTVY0xsbXue74DdVsSFihERNQiSdUJ9aHboT50O2RuNuTGL6s2Zu5slvcUraKgvLIcyjP/r+a25K6uhZxqc5uEhNV+oAt84GBLwAKFiIj8nty+FTI327PxxFFtUV0wA4hrW+N14rbfez0WYTZDtGkHMe6cyzeRVtfXrGOQ5ZVzsNQ2FwsAtLJ6PS5/0+zP4iEiIvImmXMS6oL/g7h+OIQ1BuoHr2m9Eh6375YWVy1nnwCKi2ocS3Tr3Wxxip794Z4OTZnyAhAcrG2TK96B+N0fgfw8V0PXnsDeqvnBvDkuxl+xQCEiIr+ivrcQOHUCcslLOHc+VHXztxAX9QEyf4P6//7psU1++kHNg4WF12zzEhEaBnH3g4DJDJGUBllwpiqWrz6FvLjagwTP6UkRdcyeHkhYoBARkX8pPlvnJvn63BpFSw2JScDxQ67lNu29FlZtlOuGV62EhHpsU1+YWrXfTXcDMbFQly6Gct+fmjUmf8EChYiI/MuR/Rf0cuXJ5yDXrYDo2d+rd/A0RJxToHi4+FIIIWCa/M+69wkwHCRLRER+o/plkvMlgoOhDLsHolOqFyJqGuWfi2o2hoZBCKF7LEbHAoWIiPyG/OyjujcmpUF5/k0oT8wGAIiBV0KZ/47HLuL6O5ozvAaJWh4AqEyd54NIjI+XeIiIyG/I44drtCmvrgScDghz5cBSa0yNh/Fp+971QPMF10jKlLlQn/1bVUN8zdufiT0oRETkJ+TZwqpbcdN6QHl8FkyvrYIQoqo4qYUY6yoGxB8e1SPMBomkLp7rvLxTKxYoRERkeDLzN6iP3a+tiy4XQXTt2ajXKoOuhPLiB1CGXNdc4TWZe6ZZ8eBffBuIgfESDxERGZ76/N891sUNTRtLIuqaUt5HRJt2dV6GIhf2oBARkaHJXT97rIsxfzVcwUHexx4UIiIyJJmbDXXJS8Ce7Vqb8uh0iN4DfBgV6YUFChERGY6026FO+aNnY8dkFicBhJd4iIjIeNxT0VejTJ7jg0DIV1igEBGRT6mbv4X66vOQFRVVbW+9qC2LAUOgvLoSIii4tpdTC8VLPEREAUxKCRQVAGUlEAmJvonh9bmuhdh4YPgfoM6YAJw4om1X/viET+Ii32KBQkQUAKSUgNMJYTZr63L5kqqp401mKDNfgohvp29chflVy2s/Atp39ixOnn1V13jIOFigEBG1ENJhdy2cPAZ0SIIQAurnK1xFSHERAEBcfi1kbnbVjKxuTgfkxnUQd4zSLd7yX3+Ec9p4jzatN6WSiEvQLR4yFhYoRER+Tl3/CeQHr9Xc0K03sPtXjya5cV2dx5FrP4Icdk+zj/WQR/bD+cxjyG1gP2XhimaNg4yNg2SJiPyIdDggf90K+dvPkKrq+lpbcQLUKE5qo0z4B8S1t2nr6hMPuC4HNQNZXgbnI3dBfeaxhuN65SMIxdQscZB/YA8KEZGfkMcOQZ05saohOhbIz2vwdeLSqyDufQjqn+8BUDmuIygYEAIi0gqk94Jct9K1c2kJ1OeehKmeW3qllBBCaF8bjLswH+rjtV86ElfeAMS2AcIiIN9+2dU2bGS9D/+jwMAChYjI4NTvvoBcvkQbR6KppThRXlkOnDis9VKIS66CMvqvAFDns1+EJQjinnFVPTH7d0M6HNqAWo9YvlkL+e4rVe/3xGyItO7aujx+CJAA2nWEunAOcDoHOHqwZpwPTkT7u+5HVlaWa8DuL/+Du99G9LmkrlRQAGGBQkRkAFJKIPs4IJTKyzb13L1yUV+g2vNpxMixUIbeWrW9U2qTH0QnuvVG9Qs76uwnoEydCzidwKFMIKIV5DdrIL9e4/E69fm/Q3nxA+BUFtRn/trg+yhPzAYSO0EJb+W5oddAILEz0DoeolNKk2KnlokFChGRAchlb0J+0cCg0JR0KBOfhggNg8w6DvnbNojLroEIi7jg9xftOkKZOhfqrL+5Go4egPrQ7Y16rfvSUb3Hv/IGKPeNr3u7osD01IJGvR8FBhYoREQ+5hx3a4P7KI9MBXoP1MZ8iLaJEG29O7Ga6NylUfspk+dApHaD+tpcyC3f1r/vC28BrSI54JWajAUKEVEzUr9cCfnfRUBUDFB4xjUw9YY7IffugEjvDZGcVuvrlNmvQcS20TlaQNw/HvLt/9S6TZk8BwgKhuiY7Fof9zeoYWGQ36x1rU95ASIpDXLnNsjis1AuydArbGqBhGyu+8l0kJubC7vd7uswWjwhBNq2basNZqPmx5zry9v5lmWlUF+cCezb1aTXKQs/hlCMMfuDzDkJ9e3/QPQeCJHeE4iJhwgLr33fM3mANbpJvST8GdefEXJusVgQFxfXqH3Zg0JE5AVSSiDrGFBwGur8p5r8ejFyrGGKEwAQ8e1g+tszjds3JraZo6FAxAKFiKiRZEWF6zJNYT5QlA9163fAL1sAp6PuF7VpD+XJORARka5jSAnknQKiol0P6SsvhUhM0ucEiPyITwuUzz77DJ988gkKCgrQqVMnjB49Gqmpqb4MKSBIhx2w2SpXJHD6FJB/2vXQrrxsIP8MYDZD9BsMqE5Iux32Pv0hTUG+DZzIS2R5KXBoH2CrgCw+C2EyobR1a8ioWMiQECD3FJCf57p0UZgPVJRBbtng+n9TXzECAGERQGkxYDJBeW4xRGS0x2YhBOB+vowPxpgQ+QufFSibNm3CkiVLMG7cOHTp0gWrV6/GrFmz8K9//QtRUVE+icn9oC1htkA6nYDDDggBCAVQlMplAdgqXHMDSAlAAqoEVCdgt7naVafrq3tZSte/inKgrNT1Pg47UF4OlJW4Jl+y210ffLHxQFSM67ZBk8k1J0JuFlBy1nX8igrXh2VFBVBw2nVMpwNQVdcHY0Rk5esERHCIq11KyJKzQHmZ6zi52ZWxN5CPas/syHYvhIa54kvs7JqJ0myBLMp3HVNVgehY190FXS4C4tq6PtxDQoFWkUBktMc1bKmqrlwKBbCYXXkEAIcNKD5bNSlVeCvAbHHlzOEAKspc28rLIMvLXLkLCnKdb3CI6/wrKiALzrj2M5tdr1dVVw5VJ2AyA5YgiNbxrtiCgl05CQoBQkIAxeTa124H7BWVX23aP2mzAYf2Qh7MrPy+lAEdUyDadnC9n3tslIDr/MIigIhWgMnkmiHT4n7WiYQsK3Gdl8lc+f1SIYKCUBoXD/VUtuscLUGueM0WwGJx7euewVNRXG8kKt9QnLPs8U2t/JlF5RdZ+bOrOgGn6np/1el6cJzT6Vo3m6t+hqV7H7XytZXLTrsrJsVUtU955ffJ6XS1SemKxz1Oofox3Ntl9WO71rX/i4qp8lzhynd55T+lMr/lZa5tJpPrn9ni+nrqZNWyw+6KqfisZ1oAnG7wf8Q52nZwjc1I6QaR3BU4Wwgkdm7259gQBQqfDZKdMmUKUlJSMGbMGACAqqr405/+hBtvvBG33367x752u91jMKwQAqGhocjNzYXD0cBfM02gbvoK6uL5rg8yp9NrxzW80DAgvh1ERCTkySNAfrWPapMJiIlzFSDe4C72FMX1y4KD48hXTGZXcZ/aDcJsgdnpgN09qNVkdj0NODrW1ctRWdyI9N4QvQcAQKOmeKe6CSGQkJCA7OxsDpLViRFybjabjT1I1uFw4ODBgx6FiKIo6NmzJzIzM2vsv2LFCixbtkxbT0pKwpw5cxp9ko111iRQAJx3cSIq/3oXJjNgMkO4/5ITrr/6lJAQiPAICEswhMUCERwCJSISSqtIiJBQQACOY0eglpyFWlwEaauAWlKMoKQ0KFFWiNAwKCFhECEhECGhMFljoEREAooCYTJDLS6CszBf+4tULS2FLCsFFAXmhHZQwiIgwsJhssbAnNC+MmjhirWewXlSSjiOHkT5L1u0nhlZUQ5ZXga1rBQmawxM0a0Bkxm2/btR9t0610PMbBUwRVqhlpXCmZdT2Zukuv5SbyjFZgtMUdGQUoVakA8owtV7YDJDCQmFEmmFEhYOERoGERwMWVHhiqe8DHA6IYKCYIqJgxJphbRXQFZUQAQHQ1iCAZMCOByQ5WVw5GRBPVsIWV4OKApkRRlkeblrmu/gYNcU4EHBlf+CXP8swa5tQcFQYuIQOmgI1KJCOHOy4Mw/Dak6q/6KruwhUM8Wud7HVg5IQNoqKp9jAlfPSOX+QiiAyQRps0HaK1w/IyFh2jlIux3SbgMcdkhI15/+qgrXQSvXK5dl9XWth6XyF6sQVW3un1XFBGFSKr+aXT0niitXVT2JwnWnRmWRKdxfTWZId09e5b5KWBiUSCuEJcj1XooAVBVSVStjcB0Pouo4rh6WyvVqxaywBLl6dVTV9X8pNAwiNBxKaKgrJ2WlEKFhWrzSYYd0OFzLlXGZWsdDhIZCiYiEuU1bKLVMbialdP2sKII9ITpJSEjwdQgBx19y7pMCpaioCKqqwmq1erRbrVacPHmyxv7Dhw/HsGHDtHX3Xy7e7kGRA66AqXt/12UHi8XVrQ53N3a17mf3ZYTqH7CNeWAWgKbWrAJAo2+kjmnC9ezTZxofgxBI6JSC08HhDVfdyd2A6+5A5a8/969GmAHI0mLXJSrAVawoJiAs3PWL1WGHdknCZAKCQyCEcB2nlgeSSdRf40gAaqPPUPvdjfq+i3V9/yoAoD2Abn2b8I5Vx6w1nmp/6TjP8y+dpvx9L8/52uIVnnX9q+TO96lTp/jXvE6M8Nd8oDFCzg3fg9JUFosFFkvtT7b0apKFAoRHuP41USD8B5Puv8rPV2i4619tgkNqfb/algPJBeecmoT51h9zrj9/yblPbrqPjIyEoigoKCjwaC8oKKjRq0JERESBxycFitlsRnJyMnbu3Km1qaqKnTt3Ii2t9mmfiYiIKHD47BLPsGHD8PLLLyM5ORmpqalYs2YNKioqkJGR4auQiIiIyCB8VqBcdtllKCoqwtKlS1FQUIDOnTtjypQpvMRDREREvh0ke8MNN+CGG27wZQhERERkQMZ5MhURERFRJRYoREREZDgsUIiIiMhwWKAQERGR4bBAISIiIsNhgUJERESG4xfP4qmL2ezX4fsd5lt/zLm+mG/9Mef682XOm/LeQvrDE4OIiIgooPASDzWorKwMkydPRllZma9DCRjMub6Yb/0x5/rzt5yzQKEGSSlx6NAhv3g8d0vBnOuL+dYfc64/f8s5CxQiIiIyHBYoREREZDgsUKhBFosFd911FywWi69DCRjMub6Yb/0x5/rzt5zzLh4iIiIyHPagEBERkeGwQCEiIiLDYYFCREREhsMChYiIiAyHBQoREREZDgsUIiIiMhwWKAGuqKgIJSUlUFUVALSv1HxsNpuvQwgoWVlZWLVqFU6ePOnrUAIGf8b1l5ubi9OnTwNoOZ/jfM51gHI4HFi8eDF2796N8PBwtG3bFn/605+gKKxZm4vD4cA777yDvLw8hISE4JprrkG3bt18HVaLpaoqFi9ejK+//hqXX3450tLS0K5dO1+H1aI5HA688cYbyM3NRWRkJK677jp06dIFQghfh9aibd26FS+88AL69euHSZMmtZjP8ZZxFtQk2dnZ+Pvf/46srCyMGTMGffv2RWZmJlatWuXr0FqsLVu24M9//jOOHDmC7t274/Dhw3jvvffwv//9z9ehtViffvopjhw5gqeffhp/+tOfkJ6eDgB+86A0f1NQUICpU6fi6NGj6NevH44cOYLXXntN+1xpKX/VG9H+/fuRmpqK06dPa58pLSHf7EEJQD///DNCQkIwefJkhISEID09Hbt370ZYWJivQ2uRsrOz8d133+Gqq67CiBEjAACDBw/G/PnzkZ2d7ePoWh4pJSoqKrBlyxZcddVV6NKlCzIzM3H06FEkJiaic+fOCAkJ8XWYLc6ePXvgcDgwefJkxMTEYMiQIVi9ejWWLl2Kiy++GB06dICUkr0pXqSqKhRFQWlpKVJSUmCz2bB27Vr0798fZrPZ7/PNHpQA4q6oz549i4KCAu1DuqCgACUlJQgODsaJEyd8GWKL4v5L3eFwoFOnTsjIyADg+j5ERkZCURQWKM1ACIH8/HycOnUKffr0wZIlSzB37lx8++23mDt3Lp5//nmUlpb6OswWw/25UlRUhOLiYsTExAAAwsLCcO211yI9PR2vvvoqAPj1L0sjUhQFUkpkZ2fjiiuuwMCBA3H27Fl88cUXAACn0+njCC8MC5QW7ssvv8TGjRuRlZWlXZfs3LkzbDYbZs2ahQULFuDPf/4zzGYz1qxZg5kzZ+Krr74CwK7w87V//34AVflLTEzEXXfdhfj4eACuDxWHwwGbzYa0tDSfxdlSuPNdvUu7devWaNWqFT744APk5uZi+vTpmDRpEqZPn46DBw9i+fLl/Pm+AP/73/+wfft25Ofna58riqLAarVi9+7d2n5WqxW33347Dhw4gO3btwPg58r5qp5zN1VVIYSAoiiw2+3o0qULBg4ciK+//hoLFizAp59+Crvd7sOoLwwv8bRQv/zyC1566SXExMSgpKQEZrMZ1157LYYNG4b+/fsjNjYWx48fx0cffYSJEyfikksuQUlJCT7//HO8++67uPLKK2EymXx9Gn5ly5YtWLRoERwOB2bPno34+HitCxaAR3erw+FAUVEROnTo4MuQ/Vp9+XYXfz/88AN69uypDY5t1aoV7r//fixZsgQjRoxAUFCQj8/Cv2zYsAFvv/024uLikJOTg7Zt22LYsGEYNGgQUlJSsGbNGuzduxddunSB2ez69dKhQwf06dMHGzZsQK9evdiL0kS15fyWW27BwIEDoSgKiouLcejQIS3nFRUVOHnyJLKysjBs2DC/eXJxbdiD0kJ99dVXGDhwIJ577jlMmzYNQ4cOxdtvv41t27YBAJKTk1FcXIzw8HBccsklkFIiPDwc3bp1g81m0/4qpcb57rvvsGLFCnTr1g3t27fHxx9/DAAeo+mrfzDv2bMH5eXlaNu2rdZWUFCgV7h+r6F8R0REoEePHjCbzR5FIuD6hWk2m3H8+HFfhO6XnE4n1qxZgxUrVuB3v/sdZs6ciSeeeAJt2rTBV199BZvNhqSkJKSnp2PLli3Yu3ev9lqr1QqTycTCpInqy/n69eu1nhGbzYaLLroImzdvxuOPP44NGzagZ8+eiIuL8/vpI1igtCDurtOcnBzs2LEDAwcOBACt4h48eDDeeecd5OTkAADsdjsiIyNRWlqqfXjs2bMHycnJ6Ny5s0/Owd+4/+MnJCSgZ8+euO+++9C/f3/s2rULv/32m8c+1W3ZsgUXXXQRIiIicOjQIcyYMQOvv/66336Q6KUx+XY4HACA/v37Y8iQIdi2bRu2b9+uFSl79uxB586d+TPeBBUVFSgqKsKVV16JjIwMmM1mdO3aFYmJiSgtLdVyPmLECDidTnz55Zc4c+aM9nqbzYaIiAhfhe+XGsq5e3yJqqr44Ycf8NJLL6Fbt25YsGAB7rvvPsTFxWHJkiUA4Le3HfMSTwuQlZWFhIQErciwWq0wm83Iy8sD4PrANpvNGDt2LB566CFs3rwZt912G6xWKwoLCzFv3jwMHToUP//8M7Zt24aRI0ciODjYl6dkeO6cu//jd+nSBcnJyTCZTOjbty/27NmDVatWoXv37tpANvf3R1VVFBQUID09HYsXL8bnn3+OIUOG4OGHH/bbD5Lm1pR8u3tNQkJCcNNNN6G4uBjPP/88evfuDYvFgl9//RX33ntvje8Lear+uRIWFoZLLrkEHTt2hKIoWq9UbGwsKioqtEtlVqsVw4cPx9q1azF9+nTceOONOHz4MA4ePIjhw4f7+IyMryk5d19Ci42NxcSJExEfH4/U1FQAQHh4OAYMGICysjLtD1d//DkXkiOW/NamTZvw7rvvwmKxICwsDEOHDsXVV1+N8vJyvP766ygsLMTkyZNhNpu1IuW9997Dxo0b8Z///AeAq6v8yy+/hJQSoaGhGDVqFCezqkddOQc8x5h8/fXX+OSTT3DLLbfgqquu8rjMkJeXh0ceeQQAkJaWhoceegiJiYm+OSGDO998O51OjzFU69atw6lTp1BUVITbb7+dP+P1qC/nADx+lhcsWACz2Yzx48drnzEAcObMGSxbtgyFhYVwOBz8XGmAN3Lu5v5/ce6lTX/EHhQ/tX37drz77ru49dZb0aZNG2zfvh2vvfYaVFXF0KFD0aNHD6xduxbffPMNhg4dqn2QDxo0CF999ZU2sc+QIUNw2WWX4ezZs7Barb49KYOrL+dXXHEFgoKCtF+MvXv3xt69e/HFF1/g0ksvRUhIiPZhUlZWhksvvRRXX301evXq5evTMixv5RsArr32Wh+fjX9oTM6FEJBSwm6349ixY7jlllsAwOMXZUxMDP74xz/CZrNxIHIDvJVzd0Hi/qz39+IEYIHid9zVcWZmJlq1aoVrrrkGZrMZffr0gc1mw7p16xAXF4dBgwZh+/bt+Pbbb9GrVy/tFtdTp07BZDIhMjJSO6bJZGJxUo+Gcr5+/XpERkZi4MCB2l/tMTExGDhwII4cOYJVq1Zh0KBBeP/99zF27Fh06NABf/nLX3x7Ugbm7XzHxsb6+IyMryk5d/8CLC4uRmlpKbp06QLAdXniiy++wKhRo7Tjsjipm7dz3hIKknO1vDNq4dw/qMePH0ebNm20yzcAcM899yA4OBjfffcdFEXBDTfcACEE/v3vf2Pv3r3Iy8vDzz//jOTkZBYkTdBQzi0WC7Zu3ardheMeyNm9e3ekpKTgo48+wpNPPgmn04moqCifnIM/Yb7119ScA8COHTsQGxuL6OhovPHGG3jssceQm5sLh8PBuU4agTlvGHtQDG779u348ccf0aZNG3Tt2lUbBNWjRw+8/fbbUFVV+8GOiIjAFVdcgVWrVuHQoUNIT0/Hww8/jAULFuCVV15BSUkJYmJiMHHiRP5lU4/zyfknn3yCkydPwmq1QlEUlJeXY/369fjyyy9x0UUX4cEHH0THjh19fGbGxHzr73xzfuLECVitVkgpsW3bNhw9ehSPPPIIrFYrnnnmGaSkpPj4zIyLOW869qAYVH5+Pv75z3/ixRdfRHFxMb7++ms888wz2vwkF110EUJDQ/Hhhx96vG7o0KEoLy/X9mvXrh2efvppTJkyBZMmTcKcOXM4WK0OF5LzsrIyHDp0SGvLy8vDpk2bMH78eDz11FP8ZVkL5lt/F5rzw4cPA3DdNmyz2RASEoIxY8Zg7ty5LfoX5YVgzs8fe1AMqKKiAu+99x5CQkIwa9YsbfzIlClT8MUXXyA1NRXR0dG47rrrsHz5clxzzTWIjY3Vrmm2a9fOYxKq4OBgxMfHa8ehmryR82PHjmnHS0xMxKxZs3x1OobHfOvPmzkPDg7GiBEjkJyc7MtTMjzm/MKwB8WAgoODYbFYkJGRgfj4eG1Cnr59++LEiRPaLcGXX345kpKSMH/+fOTm5kIIgby8PBQWFmqTtAH+ef+73rydc6of860/b+c8kH5Rni/m/MJwHhSDqn6LpPv2sQULFiA4OBgPPfSQtt+ZM2fw9NNPw+l0IiUlBXv37kX79u0xYcIEDoRtIuZcX8y3/phz/THn548Fih+ZPn06rrnmGmRkZGh3LiiKguzsbBw8eBD79u1Dp06dkJGR4dtAWxDmXF/Mt/6Yc/0x543DMSh+4tSpU8jOztYG/ymKAofDAUVRkJCQgISEBFx22WU+jrJlYc71xXzrjznXH3PeeByDYnDuDq49e/YgJCREuwb54Ycf4o033kBhYaEvw2uRmHN9Md/6Y871x5w3HXtQDM49wHX//v3a7LALFy6EzWbDo48+yomomgFzri/mW3/Muf6Y86ZjgeIHbDYbfv31V5w6dQpr167F3Xffjdtvv93XYbVozLm+mG/9Mef6Y86bhgWKHwgKCkJcXBx69eqFP/zhD5wFVgfMub6Yb/0x5/pjzpuGd/H4iZbw6Gx/w5zri/nWH3OuP+a88VigEBERkeGwjCMiIiLDYYFCREREhsMChYiIiAyHBQoREREZDgsUIiIiMhwWKERERGQ4LFCIiIjIcDiTLBF5zTfffIP//Oc/2rrFYkFERAQ6duyIvn374qqrrkJoaGiTj7t37178+uuvuPnmmxEeHu7NkInIoFigEJHXjRgxAvHx8XA6nSgoKMCuXbvw1ltvYfXq1Zg0aRI6derUpOPt3bsXy5YtQ0ZGBgsUogDBAoWIvK5v375ISUnR1ocPH46dO3fin//8J5577jnMnz+fzyEhonqxQCEiXfTo0QN33nkn3n//fWzYsAFDhw7FkSNH8Omnn2L37t3Iz89HWFgY+vbti/vvvx+tWrUCACxduhTLli0DADz66KPa8V566SXEx8cDADZs2IDVq1fj+PHjCAoKQu/evXHfffchNjZW/xMlIq9ggUJEurniiivw/vvvY/v27Rg6dCi2b9+OnJwcZGRkwGq14vjx4/jyyy9x/PhxzJo1C0IIDBo0CFlZWfj+++8xatQorXCJjIwEACxfvhz//e9/cemll+Kaa65BUVER1q5di6eeegrPPfccLwkR+SkWKESkm9atWyMsLAynTp0CAFx//fW45ZZbPPbp0qUL/v3vf2PPnj3o1q0bOnXqhKSkJHz//fcYMGCA1msCALm5uVi6dClGjhyJO+64Q2sfOHAgJk+ejM8//9yjnYj8B28zJiJdhYSEoKysDAA8xqHYbDYUFRWhS5cuAIBDhw41eKzNmzdDSonLLrsMRUVF2j+r1YqEhAT89ttvzXMSRNTs2INCRLoqLy9HVFQUAKC4uBgffvghNm3ahMLCQo/9SktLGzxWdnY2pJSYMGFCrdvNZn7EEfkr/u8lIt2cPn0apaWlaNOmDQBg/vz52Lt3L2699VZ07twZISEhUFUVzz77LFRVbfB4qqpCCIG///3vUJSaHcIhISFePwci0gcLFCLSzYYNGwAAffr0QXFxMXbs2IERI0bgrrvu0vbJysqq8TohRK3HS0hIgJQS8fHxaNeuXfMETUQ+wTEoRKSLnTt34qOPPkJ8fDwuv/xyrcdDSumx3+rVq2u8Njg4GEDNyz4DBw6EoihYtmxZjeNIKXH27FlvngIR6Yg9KETkdT///DNOnDgBVVVRUFCA3377Ddu3b0dsbCwmTZqEoKAgBAUFoVu3bli1ahWcTidiYmLw66+/Iicnp8bxkpOTAQDvv/8+Bg8eDJPJhH79+iEhIQH33HMP3nvvPeTm5mLAgAEICQlBTk4Otm7dimuuuQa33nqr3qdPRF4g5Ll/dhARnadzn8VjNpu1Z/FcfPHFNZ7Fc+bMGSxevBi//fYbpJTo1asXHnzwQTz00EO46667MGLECG3fjz76COvWrUN+fj6klB4TtW3evBmrV6/W7vyJjY1Fjx49cOONN/LSD5GfYoFCREREhsMxKERERGQ4LFCIiIjIcFigEBERkeGwQCEiIiLDYYFCREREhsMChYiIiAyHBQoREREZDgsUIiIiMhwWKERERGQ4LFCIiIjIcFigEBERkeGwQCEiIiLD+f+66/0DgTVRwQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import yfinance as yf\n", + "data = yf.download(\"NVDA\", start=\"2005-01-01\", end=\"2024-06-20\", back_adjust=True, auto_adjust=True, multi_level_index=False)\n", + "data.plot(y=\"Close\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "626b8ce6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:15 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:15 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2024-06-20...\n", + "2026-02-02 20:26:15 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:15 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2024-06-20...\n", + "2026-02-02 20:26:16 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:16 [test] trade.datamanager.utils INFO: Sanitizing data from 2005-01-01 to 2026-01-30...\n" + ] + }, + { + "data": { + "text/plain": [ + "TimeseriesData(spot=True, chain_spot=True, dividends=True, additional_data_keys=[])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ts.get_timeseries(\"C\", start_date=\"2005-01-01\", end_date=\"2024-06-20\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ff422a84", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2026-02-02 20:26:16 [test] trade.datamanager.utils INFO: Cache hit for timeseries data structure key: C\n", + "2026-02-02 20:26:16 [test] trade.datamanager.utils INFO: Sanitizing data from 2020-01-01 to 2024-06-20...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[autoreload of trade.datamanager.rates failed: Traceback (most recent call last):\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 276, in check\n", + " superreload(m, reload, self.old_objects)\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/site-packages/IPython/extensions/autoreload.py\", line 475, in superreload\n", + " module = reload(module)\n", + " ^^^^^^^^^^^^^^\n", + " File \"/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/lib/python3.11/importlib/__init__.py\", line 169, in reload\n", + " _bootstrap._exec(spec, module)\n", + " File \"\", line 621, in _exec\n", + " File \"\", line 936, in exec_module\n", + " File \"\", line 1074, in get_code\n", + " File \"\", line 1004, in source_to_code\n", + " File \"\", line 241, in _call_with_frames_removed\n", + " File \"/Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/datamanager/rates.py\", line 525\n", + " data_min.columns = data_min.columns.str.lower()\n", + " ^^^^^^^^\n", + "IndentationError: expected an indented block after 'except' statement on line 523\n", + "]\n" + ] + }, + { + "data": { + "text/plain": [ + "(datetime\n", + " 2020-01-02 1.0\n", + " 2020-01-03 1.0\n", + " 2020-01-06 1.0\n", + " 2020-01-07 1.0\n", + " 2020-01-08 1.0\n", + " ... \n", + " 2024-06-13 1.0\n", + " 2024-06-14 1.0\n", + " 2024-06-17 1.0\n", + " 2024-06-18 1.0\n", + " 2024-06-20 1.0\n", + " Name: split_factor, Length: 1124, dtype: float64,\n", + " False,\n", + " '2020-01-01',\n", + " '2024-06-20')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "_data_structure_cache_check_missing(\n", + " cached_data=ts._split_factor.get(\"C\"),\n", + " start_dt=\"2020-01-01\",\n", + " end_dt=\"2024-06-20\",\n", + " key=\"C\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f5ceb061", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from datetime import datetime, timedelta\n", + "t = get_div_schedule(\"AAPL\")\n", + "def resample_dividends_to_daily(div_series: pd.Series, buffer: int = 30) -> pd.Series:\n", + " \"\"\"Resample dividend series to daily frequency with forward fill.\"\"\"\n", + "\n", + " freq = infer_frequency(div_series)\n", + " if freq is None:\n", + " raise ValueError(\"Could not infer frequency.\")\n", + " freq_days = FREQ_MAP[freq] * 30 # Approximate to days\n", + " freq_days += buffer\n", + "\n", + " ## First, resample to 1b (daily business days)\n", + " resampled = div_series.resample(\"1b\").ffill()\n", + "\n", + " ## Next, the resampled is clearly missing last dividends to today or end_date,\n", + " ## SO we will forward fill the last known dividend to today. But with some rules.\n", + " ## There are cases where dividends were discontinued, so we will only forward fill if the last known dividend date - today is less than freq_days\n", + " ## If not we fill with zeros\n", + " last_div_date = div_series.dropna().index[-1]\n", + " today = datetime.now()\n", + " days_since_last_div = (today - last_div_date).days\n", + "\n", + " ## Add additional days to ffill into\n", + " resampled = resampled.reindex(pd.date_range(start=resampled.index[0], end=today, freq=\"1b\"))\n", + " if days_since_last_div <= freq_days:\n", + " resampled = resampled.ffill()\n", + " else:\n", + " print(\"Filling with zeros as dividends seem to be discontinued.\")\n", + " resampled.loc[last_div_date + timedelta(days=1): today] = 0.0\n", + " return resampled" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f71a2217", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "from openbb import obb\n", + "import pandas as pd\n", + "from trade.optionlib.config.defaults import (\n", + " OPTION_TIMESERIES_START_DATE,\n", + ")\n", + "from trade.helpers.Logging import setup_logger\n", + "from trade.helpers.helper import CustomCache\n", + "from dataclasses import dataclass\n", + "from trade.datamanager.vars import DM_GEN_PATH\n", + "\n", + "logger = setup_logger(\"trade.optionlib.utils.market_data\")\n", + "\n", + "\n", + "@dataclass\n", + "class SavedDividendsResult:\n", + " symbol: str\n", + " historicals: pd.Series\n", + " resampled_timeseries: pd.Series\n", + " last_updated: datetime\n", + "\n", + "## Cache has to be in memory. Incase dividends update on another date\n", + "DIVIDEND_CACHE = CustomCache(\n", + " location=DM_GEN_PATH,\n", + " fname=\"discrete_dividends_timeseries\",\n", + " clear_on_exit=False,\n", + " expire_days=365\n", + ")\n", + "\n", + "\n", + "def get_div_schedule(ticker, \n", + " filter_specials=True):\n", + " \"\"\"\n", + " Fetch the dividend schedule for a given ticker.\n", + " If the ticker is not in the cache, it fetches the data from yfinance and caches it.\n", + " If the ticker is in the cache, it retrieves the data from the cache.\n", + " If filter_specials is True, it filters out dividends >= 7.5.\n", + " Returns a DataFrame with the dividend schedule.\n", + " \"\"\"\n", + "\n", + " ## We're going to use a multi-level dividend retrieval. CustomCache is on disk cache\n", + " ## 1. We first check if the symbol is in the on disk DIVIDEND_CACHE\n", + " ## 2. If not, we fetch from yfinance via openbb and store in DIVIDEND_CACHE and save with last_updated\n", + " ## 3. If in cache, we retrieve from cache, but still check last_updated. \n", + " ## We will use a weekly update policy to refresh dividends\n", + " ## 4. Return the dividend schedule DataFrame\n", + "\n", + " # Check if ticker is in cache\n", + " key = (ticker, filter_specials)\n", + " if key not in DIVIDEND_CACHE:\n", + " try:\n", + " div_history = obb.equity.fundamental.dividends(symbol=ticker, provider=\"yfinance\").to_df()\n", + " div_history.set_index(\"ex_dividend_date\", inplace=True)\n", + " div_history[\"amount\"] = div_history[\"amount\"].astype(float)\n", + " div_history.index = pd.to_datetime(div_history.index)\n", + " dividends_data = SavedDividendsResult(\n", + " symbol=ticker,\n", + " historicals=div_history[\"amount\"],\n", + " resampled_timeseries=None,\n", + " last_updated=datetime.now(),\n", + " )\n", + " except Exception as e: # noqa\n", + " div_history = pd.DataFrame(\n", + " {\"amount\": [0]}, index=pd.bdate_range(start=OPTION_TIMESERIES_START_DATE, end=datetime.now(), freq=\"1Q\")\n", + " )\n", + " dividends_data = SavedDividendsResult(\n", + " symbol=ticker,\n", + " historicals=div_history[\"amount\"],\n", + " resampled_timeseries=None,\n", + " last_updated=datetime.now(),\n", + " )\n", + " DIVIDEND_CACHE[key] = dividends_data\n", + "\n", + " else:\n", + " print(f\"Ticker {ticker} found in dividend cache.\")\n", + " dividends_data: SavedDividendsResult = DIVIDEND_CACHE[key]\n", + " # Check if we need to refresh (if last_updated > 7 days)\n", + " if (datetime.now() - dividends_data.last_updated).days > 7:\n", + " del DIVIDEND_CACHE[key]\n", + " return get_div_schedule(ticker, filter_specials=filter_specials)\n", + "\n", + " # Filter out dividends >= 7.5\n", + " if filter_specials:\n", + " dividends_data.historicals = dividends_data.historicals[dividends_data.historicals < 7.5]\n", + "\n", + " return dividends_data.historicals.sort_index()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "621cc740", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ticker T found in dividend cache.\n" + ] + }, + { + "data": { + "text/plain": [ + "2023-01-02 0.278\n", + "2023-01-03 0.278\n", + "2023-01-04 0.278\n", + "2023-01-05 0.278\n", + "2023-01-06 0.278\n", + " ... \n", + "2023-12-25 0.278\n", + "2023-12-26 0.278\n", + "2023-12-27 0.278\n", + "2023-12-28 0.278\n", + "2023-12-29 0.278\n", + "Freq: B, Name: amount, Length: 260, dtype: float64" + ] + }, + "execution_count": 53, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def get_daily_dividends_timeseries(ticker, start, end):\n", + " \"\"\"Get daily resampled dividend timeseries for a given ticker between start and end dates.\"\"\"\n", + " \n", + " # Else we fallthrough to refetching the schedule\n", + " div_series = get_div_schedule(ticker)\n", + " daily_div_series = resample_dividends_to_daily(div_series)\n", + " daily_div_series = daily_div_series[start:end]\n", + " return daily_div_series\n", + "\n", + "daily = get_daily_dividends_timeseries(\"T\",'2023-01-02','2023-12-31')\n", + "daily" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openbb_new_use", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/trade/datamanager/option_spot.py b/trade/datamanager/option_spot.py new file mode 100644 index 0000000..7185db1 --- /dev/null +++ b/trade/datamanager/option_spot.py @@ -0,0 +1,513 @@ +"""Option spot price data management with Thetadata API integration. + +This module provides the OptionSpotDataManager class for retrieving and caching +option contract spot prices from Thetadata API. Supports both EOD (end-of-day) +and Quote endpoints with intelligent partial caching. + +Typical usage: + >>> opt_spot_mgr = OptionSpotDataManager("AAPL") + >>> result = opt_spot_mgr.get_option_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C" + ... ) + >>> prices = result.daily_option_spot +""" + +from datetime import datetime +from typing import Optional, Tuple, Union +import pandas as pd +from trade.helpers.Logging import setup_logger +from trade.helpers.helper import change_to_last_busday, to_datetime +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager.result import OptionSpotResult +from trade.datamanager._enums import ArtifactType, Interval, ModelPrice, SeriesId, OptionSpotEndpointSource +from trade.datamanager.utils.data_structure import _data_structure_sanitize +from trade.datamanager.utils.cache import _data_structure_cache_it, _check_cache_for_timeseries_data_structure +from trade.datamanager.config import OptionDataConfig +from trade.datamanager.utils.date import DateRangePacket, DATE_HINT, _sync_date, is_available_on_date +from trade.datamanager.utils.logging import get_logging_level +from dbase.DataAPI.ThetaData import retrieve_eod_ohlc, quote_to_eod_patch, retrieve_quote_rt +from dbase.utils import default_timestamp +from dbase.DataAPI.ThetaData.utils import _handle_opttick_param + +logger = setup_logger("trade.datamanager.option_spot", stream_log_level=get_logging_level()) + +class OptionSpotDataManager(BaseDataManager): + """Manages option spot price retrieval for a specific symbol from Thetadata API. + + Retrieves historical and real-time option contract prices (OHLC data) from + Thetadata's EOD or Quote endpoints. Implements intelligent caching with partial + cache support to minimize API calls. + + Attributes: + CACHE_NAME: Class-level cache identifier for this manager type. + DEFAULT_SERIES_ID: Default historical series identifier. + CONFIG: Configuration object for option data settings. + INSTANCES: Class-level cache of manager instances per symbol. + symbol: The underlying equity ticker symbol. + + Examples: + >>> # Get option spot price for single date + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> result = opt_mgr.get_option_spot( + ... date="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C" + ... ) + >>> price = result.daily_option_spot["close"].iloc[0] + + >>> # Get time-series of option prices + >>> result = opt_mgr.get_option_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C", + ... endpoint_source=OptionSpotEndpointSource.EOD + ... ) + >>> ohlc_data = result.daily_option_spot + """ + + CACHE_NAME: str = "option_spot_manager" + DEFAULT_SERIES_ID: SeriesId = SeriesId.HIST + CONFIG: OptionDataConfig = OptionDataConfig() + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + + def __init__( + self, + symbol: str, + *, + enable_namespacing: bool = False, + ) -> None: + """Initializes manager for a specific symbol. + + Sets up the option spot data manager with persistent cache for API responses. + + Args: + symbol: Underlying equity ticker symbol (e.g., "AAPL", "SPY"). + enable_namespacing: If True, enables namespace isolation in cache keys. + + Examples: + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> opt_mgr = OptionSpotDataManager("AAPL", cache_spec=CacheSpec(expire_days=7)) + """ + super().__init__(enable_namespacing=enable_namespacing, symbol=symbol) + self.symbol = symbol + + def _sync_date( + self, + start_date: DATE_HINT, + end_date: DATE_HINT, + strike: Optional[float] = None, + expiration: Optional[Union[datetime, str]] = None, + right: Optional[str] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = OptionSpotEndpointSource.EOD + ) -> Tuple[DATE_HINT, DATE_HINT]: + """Synchronizes requested dates with available data range from Thetadata. + + Queries Thetadata for available dates for the specified option contract and + adjusts the requested date range to fit within available data bounds. + + Args: + start_date: Requested start date. + end_date: Requested end date. + strike: Option strike price. + expiration: Option expiration date. + right: Option type ("C" for call, "P" for put). + + Returns: + Tuple of (adjusted_start_date, adjusted_end_date) constrained to + available data range. + + Examples: + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> start, end = opt_mgr._sync_date( + ... start_date="2025-01-01", + ... end_date="2025-12-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C" + ... ) + + Notes: + - Constrains start_date to max(requested_start, min_available_date) + - Constrains end_date to min(requested_end, max_available_date) + - Prevents requesting dates outside available data range + """ + return _sync_date( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + strike=strike, + expiration=expiration, + right=right, + endpoint_source=endpoint_source + ) + + def get_option_spot( + self, + date: Union[datetime, str], + *, + strike: Optional[float] = None, + expiration: Optional[Union[datetime, str]] = None, + right: Optional[str] = None, + opttick: Optional[str] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + model_price: Optional[ModelPrice] = None, + ) -> OptionSpotResult: + """Fetches option spot price for a single date from Thetadata API. + + Retrieves OHLC data for a specific option contract on a single date. + Wrapper around get_option_spot_timeseries with single-date range. + + Args: + date: Target date (YYYY-MM-DD string or datetime). + strike: Option strike price. Required unless opttick provided. + expiration: Option expiration date. Required unless opttick provided. + right: Option type ("C" for call, "P" for put). Required unless opttick provided. + opttick: Optional ticker string (e.g., "AAPL250620C00150000"). If provided, + overrides strike, expiration, and right parameters. + endpoint_source: API endpoint to use (EOD or QUOTE). Uses config default if None. + + Returns: + OptionSpotResult containing daily_option_spot DataFrame with OHLC data, + plus metadata (key, endpoint_source). + + Examples: + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> # Using strike/expiration/right + >>> result = opt_mgr.get_option_spot( + ... date="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C" + ... ) + >>> close_price = result.daily_option_spot["close"].iloc[0] + + >>> # Using opttick + >>> result = opt_mgr.get_option_spot( + ... date="2025-01-15", + ... opttick="AAPL250620C00150000" + ... ) + + Notes: + - Returns DataFrame with columns: open, high, low, close, volume + - Uses EOD endpoint by default for historical data + - Quote endpoint available for more recent data + """ + date_str = pd.to_datetime(date).strftime("%Y-%m-%d") if isinstance(date, datetime) else date + result = self.get_option_spot_timeseries( + start_date=date_str, + end_date=date_str, + strike=strike, + expiration=expiration, + right=right, + opttick=opttick, + endpoint_source=endpoint_source, + model_price=model_price, + ) + return result + + def get_option_spot_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + *, + strike: Optional[float] = None, + expiration: Optional[Union[datetime, str]] = None, + right: Optional[str] = None, + opttick: Optional[str] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + model_price: Optional[ModelPrice] = None, + ) -> OptionSpotResult: + """Fetches option spot price time-series from Thetadata API. + + Retrieves daily OHLC data for a specific option contract over a date range. + Implements intelligent caching with partial cache support to minimize API calls. + + Args: + start_date: Start of date range (YYYY-MM-DD string or datetime). + end_date: End of date range (YYYY-MM-DD string or datetime). + strike: Option strike price. Required unless opttick provided. + expiration: Option expiration date. Required unless opttick provided. + right: Option type ("C" for call, "P" for put). Required unless opttick provided. + opttick: Optional ticker string (e.g., "AAPL250620C00150000"). If provided, + overrides strike, expiration, and right parameters. + endpoint_source: API endpoint to use (EOD or QUOTE). Uses config default if None. + model_price: Optional model price type to use. + + Returns: + OptionSpotResult containing daily_option_spot DataFrame indexed by datetime + with OHLC data, plus metadata (key, endpoint_source). + + Examples: + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> # Get historical option prices + >>> result = opt_mgr.get_option_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C", + ... endpoint_source=OptionSpotEndpointSource.EOD + ... ) + >>> ohlc = result.daily_option_spot + >>> print(ohlc.head()) + datetime open high low close volume + 2025-01-02 5.25 5.50 5.20 5.45 12500 + 2025-01-03 5.50 5.75 5.45 5.70 15300 + ... + + >>> # Using opttick format + >>> result = opt_mgr.get_option_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... opttick="AAPL250620C00150000" + ... ) + + Notes: + - Partial cache hits only fetch missing dates from API + - Full cache hits return immediately without API calls + - Automatically adjusts date range to available data bounds + - EOD endpoint: Historical end-of-day data + - QUOTE endpoint: Constructed from quote data (fallback for recent dates) + """ + if endpoint_source is None: + endpoint_source = self.CONFIG.option_spot_endpoint_source + + result = OptionSpotResult() + result.symbol = self.symbol + result.endpoint_source = endpoint_source + result.strike = strike + result.right = right + result.expiration = to_datetime(expiration) if expiration is not None else None + result.rt = False + result.model_price = model_price or self.CONFIG.model_price + + + strike, right, symbol, expiration = _handle_opttick_param( + strike=strike, + right=right, + symbol=self.symbol, + exp=expiration, + opttick=opttick, + enforce_single_option=True, + ) + + ## Sync requested dates with available data range + start_date, end_date = self._sync_date( + start_date=start_date, + end_date=end_date, + strike=float(strike), + expiration=expiration, + right=right, + endpoint_source=endpoint_source, + ) + + date_packet = DateRangePacket(start_date=start_date, end_date=end_date, maturity_date=expiration) + start_date, end_date = date_packet.start_date, date_packet.end_date + start_str, end_str = date_packet.start_str, date_packet.end_str + expiration = date_packet.maturity_date + + # Construct cache key + key = self.make_key( + symbol=self.symbol, + artifact_type=ArtifactType.OPTION_SPOT, + series_id=SeriesId.HIST, + endpoint_source=endpoint_source.value, + interval=Interval.EOD, + strike=strike, + right=right, + expiration=expiration, + ) + + # Check cache + cached_data, is_partial, start_date, end_date = _check_cache_for_timeseries_data_structure( + key=key, + self=self, + start_dt=start_date, + end_dt=end_date, + ) + + if cached_data is not None and not is_partial: + logger.info(f"Cache hit for option spot timeseries key: {key}") + result.daily_option_spot = cached_data + result.key = key + result.endpoint_source = endpoint_source + return result + elif is_partial: + logger.info( + f"Cache partially covers requested date range for option spot timeseries. Key: {key}. Fetching missing dates." + ) + else: + logger.info(f"No cache found for option spot timeseries key: {key}. Fetching from source.") + + # Fetch data from Thetadata API (placeholder logic) + fetched_data = self._query_thetadata_api( + start_date=start_date, + end_date=end_date, + endpoint_source=endpoint_source, + strike=strike, + expiration=expiration, + right=right, + ) + + # Merge with cached data if partial + if cached_data is not None and is_partial: + merged = pd.concat([cached_data, fetched_data]) + fetched_data = merged[~merged.index.duplicated(keep="last")] + + fetched_data.index = default_timestamp(fetched_data.index) + + # Cache the fetched data + _data_structure_cache_it(self, key, fetched_data) + + # Sanitize before returning + fetched_data = _data_structure_sanitize( + fetched_data, + start=start_str, + end=end_str, + source_name=f"option spot timeseries for {self.symbol} with strike {strike}, right {right}, expiration {expiration} from {endpoint_source.value}", + ) + + result.daily_option_spot = fetched_data + result.key = key + result.endpoint_source = endpoint_source + return result + + def _query_thetadata_api( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + endpoint_source: OptionSpotEndpointSource, + strike: Optional[float] = None, + expiration: Optional[Union[datetime, str]] = None, + right: Optional[str] = None, + ) -> pd.DataFrame: + """Fetches option spot data from Thetadata API using specified endpoint. + + Makes HTTP requests to Thetadata's EOD or Quote endpoints to retrieve + option contract OHLC data. + + Args: + start_date: Start of date range (YYYY-MM-DD string or datetime). + end_date: End of date range (YYYY-MM-DD string or datetime). + endpoint_source: API endpoint (OptionSpotEndpointSource.EOD or QUOTE). + strike: Option strike price. + expiration: Option expiration date. + right: Option type ("C" for call, "P" for put). + + Returns: + DataFrame indexed by datetime with columns: + - open: Opening price + - high: Highest price + - low: Lowest price + - close: Closing price + - volume: Trading volume + + Examples: + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> df = opt_mgr._query_thetadata_api( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... endpoint_source=OptionSpotEndpointSource.EOD, + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C" + ... ) + + Notes: + - EOD endpoint: Uses retrieve_eod_ohlc for historical data + - QUOTE endpoint: Uses quote_to_eod_patch (constructs OHLC from quotes) + - Quote endpoint useful when EOD data not yet available + """ + # In a real implementation, this method would make HTTP requests to Thetadata's API. + if endpoint_source == OptionSpotEndpointSource.EOD: + return retrieve_eod_ohlc( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + strike=float(strike), + exp=expiration, + right=right, + ) + + else: + logger.info( + f"Fetching option spot data from Thetadata Quote endpoint for {self.symbol} from {start_date} to {end_date}." + ) + return quote_to_eod_patch( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + strike=float(strike), + exp=expiration, + right=right, + ohlc_format=True, + ) + + def rt( + self, + strike: float, + right: str, + expiration: Union[datetime, str], + ) -> OptionSpotResult: + """ + Fetches real-time option spot price from Thetadata Quote endpoint. + + Retrieves the most recent OHLC data for a specific option contract using + Thetadata's Quote endpoint. + + Args: + strike: Option strike price. + right: Option type ("C" for call, "P" for put). + expiration: Option expiration date. + + Returns: + OptionSpotResult containing daily_option_spot DataFrame with OHLC data, + plus metadata (key, endpoint_source). + """ + as_of = datetime.now().date() + if not is_available_on_date(as_of): + as_of = change_to_last_busday(as_of - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + logger.info( + f"Real-time data not available for {self.symbol} on {as_of}. Market may be closed." + ) + res = self.get_option_spot( + strike=strike, + right=right, + expiration=to_datetime(expiration) if expiration is not None else None, + date=as_of, + ) + res.rt = True + return res + rt = retrieve_quote_rt( + symbol=self.symbol, + exp=to_datetime(expiration) if expiration is not None else None, + strike=strike, + right=right, + ) + rt.index = default_timestamp(rt.index) + rt.columns = rt.columns.str.lower() + result = OptionSpotResult() + result.daily_option_spot = rt + result.key = self.make_key( + symbol=self.symbol, + time = datetime.now().time(), + date = datetime.now(), + artifact_type=ArtifactType.OPTION_SPOT, + series_id=SeriesId.AT_TIME, + endpoint_source=OptionSpotEndpointSource.QUOTE, + ) + result.symbol = self.symbol + result.strike = strike + result.right = right + result.expiration = to_datetime(expiration) if expiration is not None else None + result.endpoint_source = OptionSpotEndpointSource.QUOTE + result.rt = True + return result + \ No newline at end of file diff --git a/trade/datamanager/rates.py b/trade/datamanager/rates.py new file mode 100644 index 0000000..bff807f --- /dev/null +++ b/trade/datamanager/rates.py @@ -0,0 +1,557 @@ +"""Risk-free rate data management for options pricing with caching. + +This module provides the RatesDataManager class for retrieving and caching +risk-free interest rates from US Treasury bills (^IRX - 13 Week Treasury Bill). +Implements singleton pattern with intelligent partial caching. + +Typical usage: + >>> rates_mgr = RatesDataManager() + >>> result = rates_mgr.get_risk_free_rate_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31" + ... ) + >>> rates = result.daily_risk_free_rates +""" + +from datetime import datetime +from typing import ClassVar, Optional, Union +import pandas as pd +import yfinance as yf +import numpy as np +from trade.helpers.Logging import setup_logger +from trade.helpers.helper import get_missing_dates, change_to_last_busday +from .utils.cache import _data_structure_cache_it +from .utils.date import is_available_on_date, to_datetime +from .utils.data_structure import _data_structure_sanitize +from .config import OptionDataConfig +from ._enums import ArtifactType, Interval, SeriesId, RealTimeFallbackOption +from .result import RatesResult +from .base import BaseDataManager, CacheSpec +from .utils.logging import get_logging_level + +logger = setup_logger("trade.datamanager.rates", stream_log_level=get_logging_level()) + + +def deannualize(annual_rate: float, periods: int = 365) -> float: + """Converts annualized interest rate to per-period rate. + + Uses compound interest formula to convert annual rate to daily rate + or other period-based rate. + + Args: + annual_rate: Annualized interest rate (e.g., 0.05 for 5%). + periods: Number of periods per year. Defaults to 365 for daily rate. + + Returns: + Per-period interest rate (e.g., daily rate if periods=365). + + Examples: + >>> # Convert 5% annual to daily rate + >>> daily_rate = deannualize(0.05, periods=365) + >>> print(f"{daily_rate:.6f}") + 0.000134 + + >>> # Convert 5% annual to weekly rate + >>> weekly_rate = deannualize(0.05, periods=52) + >>> print(f"{weekly_rate:.6f}") + 0.000942 + """ + return (1 + annual_rate) ** (1 / periods) - 1 + + +class RatesDataManager(BaseDataManager): + """Singleton manager for risk-free rate data from treasury bills (^IRX). + + Manages retrieval and caching of US Treasury Bill rates (13-week T-Bill) used as + risk-free rates in options pricing. Implements singleton pattern to ensure single + instance across application. Supports partial caching with automatic cache merging. + + Attributes: + CACHE_NAME: Class-level cache identifier for this manager type. + DEFAULT_SERIES_ID: Default historical series identifier. + INSTANCE: Singleton instance reference. + DEFAULT_YFINANCE_TICKER: Yahoo Finance ticker for 13-week T-Bill (^IRX). + CONFIG: Configuration object for rates data settings. + + Examples: + >>> # Singleton access - same instance returned + >>> rates_mgr1 = RatesDataManager() + >>> rates_mgr2 = RatesDataManager() + >>> assert rates_mgr1 is rates_mgr2 + + >>> # Get rate for a single date + >>> result = rates_mgr1.get_rate(date="2025-01-15") + >>> rate = result.daily_risk_free_rates.iloc[0] + + >>> # Get time-series of rates + >>> result = rates_mgr1.get_risk_free_rate_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31" + ... ) + >>> rates = result.daily_risk_free_rates + """ + + CACHE_NAME: ClassVar[str] = "rates_data_manager" + DEFAULT_SERIES_ID: ClassVar["SeriesId"] = SeriesId.HIST + INSTANCE: ClassVar[Optional["RatesDataManager"]] = None + DEFAULT_YFINANCE_TICKER :str = "^IRX" # 13 WEEK TREASURY BILL + CONFIG: OptionDataConfig = OptionDataConfig() + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + + def __new__( + cls, + *, + cache_spec: Optional[CacheSpec] = None, + enable_namespacing: bool = False, + ) -> "RatesDataManager": + """Ensures only one instance exists (singleton pattern). + + Returns the existing singleton instance if available, otherwise creates + a new one. Ensures risk-free rate data is managed globally. + + Args: + cache_spec: Optional cache configuration. Uses default if None. + enable_namespacing: If True, enables namespace isolation in cache keys. + + Returns: + Singleton RatesDataManager instance. + + Examples: + >>> mgr1 = RatesDataManager() + >>> mgr2 = RatesDataManager() + >>> assert mgr1 is mgr2 # Same instance + """ + + if cls.INSTANCE is not None: + return cls.INSTANCE + instance = object.__new__(cls) + cls.INSTANCE = instance + return instance + + def __init__(self, *, enable_namespacing: bool = False) -> None: + """Initializes singleton instance once, skipping subsequent calls. + + Sets up persistent cache for rates data. Only executes initialization logic + on first instantiation due to singleton pattern. + + Args: + enable_namespacing: If True, enables namespace isolation in cache keys. + + Examples: + >>> mgr = RatesDataManager() + >>> mgr = RatesDataManager(cache_spec=CacheSpec(expire_days=30)) + """ + if getattr(self, "_init_called", False): + return + self._init_called = True + super().__init__(enable_namespacing=enable_namespacing) + + @property + def symbol(self) -> str: + """Returns the symbol associated with this RatesDataManager.""" + return self.DEFAULT_YFINANCE_TICKER + + @symbol.setter + def symbol(self, value: str) -> None: + """Sets the symbol associated with this RatesDataManager.""" + pass + + def get_rate( + self, + date: Union[datetime, str], + interval: Interval = Interval.EOD, + str_interval: Optional[str] = None, + fallback_option: Optional[RealTimeFallbackOption] = None, + *, + force_fail_n: int = 0, + ) -> RatesResult: + """Returns risk-free rate for a single date. + + Fetches the risk-free interest rate (from 13-week T-Bill) for a specific date. + Returns empty result if date is not a business day or is a US holiday. + + Args: + date: Target date for rate lookup (YYYY-MM-DD string or datetime). + interval: Time interval resolution. Defaults to Interval.EOD (end-of-day). + str_interval: Optional yfinance interval string (e.g., "1d", "30m"). + Overrides interval parameter if provided. + + Returns: + RatesResult containing daily_risk_free_rates Series with single value, + or empty Series if date is invalid. + + Examples: + >>> rates_mgr = RatesDataManager() + >>> result = rates_mgr.get_rate(date="2025-01-15") + >>> if not result.daily_risk_free_rates.empty: + ... rate = result.daily_risk_free_rates.iloc[0] + ... print(f"Rate: {rate:.4f}") + Rate: 0.0485 + + >>> # Weekend date returns empty result + >>> result = rates_mgr.get_rate(date="2025-01-18") # Saturday + >>> print(result.daily_risk_free_rates.empty) + True + + Notes: + - Validates date is a business day (not weekend or US holiday) + - Uses internal timeseries method with single-date range + - Returns annualized rate (e.g., 0.0485 = 4.85%) + """ + ## To avoid infinite recursion in fallback + if force_fail_n > 7: + raise ValueError("Exceeded maximum recursion attempts in get_rate fallback handling.") + force_fail_n += 1 + fallback_option = fallback_option or self.CONFIG.real_time_fallback_option + date = to_datetime(date) + + ## Resolving date availability + if not is_available_on_date(date.date()): + logger.warning( + f"Requested date {date} is not a business day or is a US holiday. Resorting to fallback option `{fallback_option}`." + ) + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"Date {date} is not available for risk-free rate data.") + + if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## Using only change_to_last_busday assumes input date is not business day or is holiday + ## Which the function would roll back + ## But there's a possibility input date is today's date but before market open + ## In that case we need to move back one more business day + date = change_to_last_busday(date - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + return self.get_rate( + date=date, + interval=interval, + str_interval=str_interval, + fallback_option=RealTimeFallbackOption.USE_LAST_AVAILABLE, + force_fail_n=force_fail_n, + ) + else: + value = pd.Series(dtype=float, + index = [pd.to_datetime(date)], + data = [np.nan if fallback_option == RealTimeFallbackOption.NAN else 0.0]) + + res = RatesResult(timeseries=value, symbol=self.DEFAULT_YFINANCE_TICKER) + res.fallback_option = fallback_option + return res + date_str = pd.to_datetime(date).strftime("%Y-%m-%d") if isinstance(date, datetime) else date + + rates_data = self.get_risk_free_rate_timeseries( + start_date=date_str, + end_date=date_str, + interval=interval, + str_interval=str_interval, + ) + + ## Date is available, but resolving no data found + rate = rates_data.timeseries + if rate is not None and not rate.empty: + rate = rate.iloc[0:1] + else: + logger.warning( + f"No rate data found for date {date}. Resorting to fallback option `{fallback_option}`." + ) + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"No rate data found for date {date}.") + elif fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## We are retaining the original date for logging and index purposes. + ## Because the returned rate should still be indexed by the requested date, which would be used in further calculations. + original_date = to_datetime(date).date() + date = change_to_last_busday(date - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + res = self.get_rate( + date=date, + interval=interval, + str_interval=str_interval, + fallback_option=RealTimeFallbackOption.USE_LAST_AVAILABLE, + force_fail_n=force_fail_n, + ) + rate = res.timeseries + rate = rate.iloc[0:1] + logger.warning( + f"Using last available rate for date {date} for {original_date} as fallback." + ) + rate.index = [pd.to_datetime(original_date)] + else: + rate = pd.Series(dtype=float, + index = [to_datetime(date)], + data = [np.nan if fallback_option == RealTimeFallbackOption.NAN else 0.0]) + + res = RatesResult(timeseries=rate, symbol=self.DEFAULT_YFINANCE_TICKER) + res.fallback_option = fallback_option + return res + + def get_risk_free_rate_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + interval: Interval = Interval.EOD, + str_interval: Optional[str] = None, + ) -> RatesResult: + """Returns risk-free rate time-series with partial cache support. + + Fetches daily risk-free interest rates from 13-week Treasury Bills for the + specified date range. Intelligently uses cache when available and only fetches + missing dates from yfinance. + + Args: + start_date: Start of date range (YYYY-MM-DD string or datetime). + end_date: End of date range (YYYY-MM-DD string or datetime). + interval: Time interval resolution. Defaults to Interval.EOD (end-of-day). + str_interval: Optional yfinance interval string (e.g., "1d", "30m"). + Overrides interval parameter if provided. + + Returns: + RatesResult containing daily_risk_free_rates Series indexed by datetime + with annualized rates. + + Examples: + >>> rates_mgr = RatesDataManager() + >>> result = rates_mgr.get_risk_free_rate_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31" + ... ) + >>> rates = result.daily_risk_free_rates + >>> print(rates.head()) + Datetime + 2025-01-02 0.0485 + 2025-01-03 0.0487 + 2025-01-06 0.0486 + ... + + >>> # High-frequency intraday rates + >>> result = rates_mgr.get_risk_free_rate_timeseries( + ... start_date="2025-01-15", + ... end_date="2025-01-15", + ... str_interval="30m" + ... ) + + Notes: + - Partial cache hits fetch only missing dates and merge with cache + - Full cache hits return immediately without external calls + - Automatically filters to business days (excludes weekends/holidays) + - Returns annualized rates (e.g., 0.0485 = 4.85%) + - Cache automatically merges and deduplicates by date + """ + + if str_interval is not None: + # normalize common yfinance strings + intraday_tokens = ["m", "h"] + if any(t in str_interval for t in intraday_tokens): + raise ValueError( + "RatesDataManager supports daily-or-higher intervals only. " f"Received str_interval={str_interval}." + ) + + if interval != Interval.EOD: + raise ValueError("RatesDataManager is EOD-only.") + + + start_str = pd.to_datetime(start_date).strftime("%Y-%m-%d") if isinstance(start_date, datetime) else start_date + end_str = pd.to_datetime(end_date).strftime("%Y-%m-%d") if isinstance(end_date, datetime) else end_date + + ## Determine yfinance interval + if not str_interval: + fn_interval = "1d" if interval == Interval.EOD else "30m" + else: + fn_interval = str_interval + + ## Make cache key + key = self.make_key( + symbol=self.DEFAULT_YFINANCE_TICKER, + artifact_type=ArtifactType.RATES, + series_id=SeriesId.HIST, + interval=interval, + fn_interval=fn_interval, + ) + + + + ## Check cache + series = self.get(key, default=None) + + ## Check if cache covers requested date range + if series is not None: + logger.info(f"Cache hit for risk-free rate timeseries key: {key}") + missing = get_missing_dates( + series, + pd.to_datetime(start_date).strftime("%Y-%m-%d"), + pd.to_datetime(end_date).strftime("%Y-%m-%d"), + ) + + ## If no missing dates, return cached series + if not missing: + logger.info(f"Cache fully covers requested date range for risk-free rate timeseries. Key: {key}") + series = _data_structure_sanitize( + series, + start=start_str, + end=end_str, + source_name=f"cached risk-free rate timeseries for {self.DEFAULT_YFINANCE_TICKER}", + ) + return RatesResult(timeseries=series, symbol=self.DEFAULT_YFINANCE_TICKER) + else: + ## Fetch missing dates + start_date = min(missing) + end_date = max(missing) + logger.info( + f"Cache partially covers requested date range for risk-free rate timeseries. Key: {key}. Fetching missing dates: {missing}" + ) + else: + logger.info(f"No cache found for risk-free rate timeseries key: {key}. Fetching from source.") + + # Fetch rates data + rates_data = self._query_yfinance( + start_date=start_date, + end_date=end_date, + interval=fn_interval, + )["annualized"] + rates_data = rates_data[(rates_data.index >= pd.to_datetime(start_str)) & (rates_data.index <= pd.to_datetime(end_str))] + # If data is empty, return empty result + if rates_data.empty: + logger.warning( + f"No risk-free rate data found for date range {start_date} to {end_date}." + ) + return RatesResult(symbol=self.DEFAULT_YFINANCE_TICKER, timeseries=pd.Series(dtype=float)) + + if series is not None: + # Merge with existing cached series + merged = pd.concat([series, rates_data]) + rates_data = merged[~merged.index.duplicated(keep="last")] + + ## Cache the updated series + self.cache_it(key, rates_data) + + ## Sanitize before returning + rates_data = _data_structure_sanitize( + rates_data, + start=start_str, # Ensure only requested range + end=end_str, + source_name=f"final risk-free rate timeseries for {self.DEFAULT_YFINANCE_TICKER} after merging cache and fetched data", + ) + + return RatesResult(symbol=self.DEFAULT_YFINANCE_TICKER, timeseries=rates_data) + + def cache_it(self, key: str, value: pd.Series, *, expire: Optional[int] = None) -> None: + """Merges and caches rate time-series, excluding today's partial data. + + Appends new rate data to existing cached time-series if cache entry exists. + Filters out today's data to avoid caching incomplete/changing values. + + Args: + key: Cache key identifier. + value: Series of rates to cache (indexed by datetime). + expire: Optional expiration time in seconds. Uses cache default if None. + + Examples: + >>> rates_mgr = RatesDataManager() + >>> rates = pd.Series([0.048, 0.049], index=pd.date_range("2025-01-01", periods=2)) + >>> rates_mgr.cache_it("my_key", rates, expire=86400) + + Notes: + - Existing cache entries are merged with new data + - Duplicates are removed, keeping latest values + - Today's data excluded to avoid caching incomplete values + """ + ## Since it is a timeseries, we will append to existing if exists + _data_structure_cache_it(self, key, value, expire=expire) + + def _query_yfinance( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + interval: str, + ) -> pd.DataFrame: + """Fetches ^IRX treasury bill rates from yfinance and formats output. + + Downloads 13-week Treasury Bill data from Yahoo Finance, processes it, + and returns formatted DataFrame with annualized and daily rates. Adds + 5-day buffer to date range to ensure complete data retrieval. + + Args: + start_date: Start of date range (YYYY-MM-DD string or datetime). + end_date: End of date range (YYYY-MM-DD string or datetime). + interval: yfinance interval string (e.g., "1d" for daily, "30m" for 30-minute). + + Returns: + DataFrame indexed by Datetime with columns: + - name: Ticker symbol (^IRX) + - description: "13 WEEK TREASURY BILL" + - daily: Daily rate (deannualized from annual rate) + - annualized: Annualized rate (as decimal, e.g., 0.0485 = 4.85%) + + Examples: + >>> rates_mgr = RatesDataManager() + >>> df = rates_mgr._query_yfinance( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... interval="1d" + ... ) + >>> print(df.head()) + name description daily annualized + Datetime + 2025-01-02 ^IRX 13 WEEK TREASURY BILL 0.000129 0.0485 + 2025-01-03 ^IRX 13 WEEK TREASURY BILL 0.000130 0.0487 + ... + + Notes: + - Uses 5-day buffer before/after date range for data completeness + - Converts yfinance percentage (4.85) to decimal (0.0485) + - Calculates daily rate using compound interest formula + - Filters final output to exact date range requested + """ + + ## Date buffer to ensure we get all data + buffered_start = to_datetime(start_date) - pd.Timedelta(days=5) + buffered_end = to_datetime(end_date) + pd.Timedelta(days=1) + yf_ticker = yf.Ticker(self.DEFAULT_YFINANCE_TICKER) + + + try: + data_min = yf_ticker.history( + start=buffered_start, + end=buffered_end, + interval=interval, + ) + data_min.index = data_min.index.tz_localize(None) + + ## Fallback in case of yfinance issues + except Exception as e: # noqa + data_min = yf.download( + yf_ticker.ticker, + start=buffered_start, + end=buffered_end, + interval=interval, + progress=False, + multi_level_index=False, + ) + + data_min.columns = data_min.columns.str.lower() + data_min["annualized"] = data_min["close"] / 100 + data_min["daily"] = (data_min["annualized"]).apply(deannualize) + data_min["name"] = self.DEFAULT_YFINANCE_TICKER + data_min["description"] = yf_ticker.info.get("shortName", "UNKNOWN") + data_min.index.name = "Datetime" + data_min = data_min[["name", "description", "daily", "annualized"]] + data_min = data_min[ + (data_min.index >= pd.to_datetime(start_date)) & (data_min.index <= pd.to_datetime(end_date)) + ] + return data_min + + def rt(self, fallback_option: Optional[RealTimeFallbackOption] = None) -> RatesResult: + """Shortcut for get_rate method. + + Provides a concise alias for retrieving risk-free rate at the current date. + + Returns: + RatesResult containing daily_risk_free_rates Series with single value + for today's date. + Examples: + >>> rates_mgr = RatesDataManager() + >>> result = rates_mgr.rt() + >>> rate = result.daily_risk_free_rates.iloc[0] + >>> print(f"Today's Rate: {rate:.4f}") + Today's Rate: 0.0485 + """ + res = self.get_rate(date=datetime.now(), fallback_option=fallback_option) + res.rt = True + return res \ No newline at end of file diff --git a/trade/datamanager/requests.py b/trade/datamanager/requests.py new file mode 100644 index 0000000..0ac9c09 --- /dev/null +++ b/trade/datamanager/requests.py @@ -0,0 +1,137 @@ +from datetime import datetime +from dataclasses import dataclass +from typing import Optional, Union +import pandas as pd +from trade.datamanager.result import SpotResult, ForwardResult, DividendsResult, RatesResult, OptionSpotResult, VolatilityResult, GreekResultSet +from trade.datamanager._enums import ModelPrice, OptionSpotEndpointSource, SeriesId, VolatilityModel, OptionPricingModel, RealTimeFallbackOption +from trade.helpers.helper import get_missing_dates +from trade.optionlib.config.types import DivType +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.logging import get_logging_level, register_to_factor_list + +logger = setup_logger("trade.datamanager.requests", stream_log_level=get_logging_level()) +register_to_factor_list("trade.datamanager.requests") + + +@dataclass +class LoadRequest: + + ## Required parameters + symbol: str + + ## Timeseries parameters + start_date: Optional[Union[str, pd.Timestamp]] = None + end_date: Optional[Union[str, pd.Timestamp]] = None + as_of: Optional[Union[str, pd.Timestamp]] = None + rt: Optional[bool] = False + on_date: Optional[bool] = False + + ## Option specific parameters + expiration: Optional[Union[str, pd.Timestamp]] = None + strike: Optional[float] = None + right: Optional[str] = None + + ## Data type + series_id: Optional[SeriesId] = None + + ## Enum types + dividend_type: Optional[DivType] = None + endpoint_source: Optional[OptionSpotEndpointSource] = None + vol_model: Optional[VolatilityModel] = None + market_model: Optional[OptionPricingModel] = None + model_price: Optional[ModelPrice] = None + fall_back_option: Optional[RealTimeFallbackOption] = None + + ## What to load + load_spot: bool = False + load_forward: bool = False + load_dividend: bool = False + load_rates: bool = False + load_option_spot: bool = False + load_vol: bool = False + load_greek: bool = False + undo_adjust: bool = True + + ## Provided inputs + spot_timeseries: Optional[SpotResult] = None + forward_timeseries: Optional[ForwardResult] = None + dividend_timeseries: Optional[DividendsResult] = None + rates_timeseries: Optional[RatesResult] = None + option_spot_timeseries: Optional[OptionSpotResult] = None + vol_timeseries: Optional[VolatilityResult] = None + greek_timeseries: Optional[GreekResultSet] = None + + def __post_init__(self): + ## Validation + + ## Dates: + if self.rt: + self.on_date = True + self.as_of = datetime.now().date() + + if all(date is not None for date in [self.start_date, self.end_date, self.as_of]): + raise ValueError("Only pass start_date and end_date or as_of, not both.") + + if all(date is None for date in [self.start_date, self.end_date, self.as_of]): + raise ValueError("Either start_date and end_date or as_of must be provided.") + + if self.start_date is not None and self.end_date is not None: + if pd.to_datetime(self.start_date) > pd.to_datetime(self.end_date): + raise ValueError("start_date must be earlier than or equal to end_date.") + + if self.as_of is not None: + if self.start_date is not None or self.end_date is not None: + raise ValueError("If as_of is provided, start_date and end_date must be None.") + self.as_of = pd.to_datetime(self.as_of) + self.start_date = self.as_of + self.end_date = self.as_of + self.on_date = True + + ## Option parameters + option_params = [self.expiration, self.strike, self.right] + option_params_str = ["expiration", "strike", "right"] + if self.load_greek or self.load_vol or self.load_option_spot: + for i, param in enumerate(option_params): + if param is None: + raise ValueError(f"{option_params_str[i]} must be provided when loading option data.") + + + if self.load_option_spot: + if self.strike is None or self.right is None: + raise ValueError("Strike and right must be provided when loading option spot data.") + + if self.model_price is None: + self.model_price = ModelPrice.MIDPOINT + + self._validate_provided_inputs() + + def _validate_provided_inputs(self): + validatees = [ + (self.load_spot, self.spot_timeseries, "load_spot", "spot_timeseries"), + (self.load_forward, self.forward_timeseries, "load_forward", "forward_timeseries"), + (self.load_dividend, self.dividend_timeseries, "load_dividend", "dividend_timeseries"), + (self.load_rates, self.rates_timeseries, "load_rates", "rates_timeseries"), + (self.load_option_spot, self.option_spot_timeseries, "load_option_spot", "option_spot_timeseries"), + (self.load_vol, self.vol_timeseries, "load_volatility", "vol_timeseries"), + (self.load_greek, self.greek_timeseries, "load_greek", "greek_timeseries") + ] + + for load_flag, timeseries, load_name, timeseries_name in validatees: + if load_flag and timeseries is not None: + if self._is_missing_dates(self.start_date, self.end_date, timeseries.timeseries): + logger.info(f"Provided {timeseries_name} timeseries has missing dates. Consider reloading without providing timeseries to fetch complete data.") + setattr(self, timeseries_name, None) + setattr(self, load_name, load_flag) # Keep the load flag as is given. Either way we will attempt to load from source, but if provided data is complete we will use it and skip loading from source. + else: + logger.info(f"Using provided {timeseries_name} timeseries for loading.") + setattr(self, load_name, False) # Prevent loading from source since we have provided data + + + def _is_missing_dates(self, start_date, end_date, series: pd.Series) -> bool: + missing_dates = get_missing_dates(_start=start_date, _end=end_date, x=series) + if missing_dates: + logger.warning(f"Missing dates in provided data: {missing_dates}") + return True + return False + + diff --git a/trade/datamanager/result.py b/trade/datamanager/result.py new file mode 100644 index 0000000..a6a9264 --- /dev/null +++ b/trade/datamanager/result.py @@ -0,0 +1,687 @@ +import pandas as pd +from dataclasses import dataclass, field +from typing import Any, Dict, Optional, List +from trade.optionlib.config.types import DivType +from ._enums import ( + GreekType, + OptionSpotEndpointSource, + OptionPricingModel, + VolatilityModel, + SeriesId, + ModelPrice, + RealTimeFallbackOption, + AVAILABLE_GREEKS +) +from .utils.date import DATE_HINT +from typeguard import check_type +from trade.helpers.helper import to_datetime +from typing import get_type_hints + + +@dataclass +class Result: + """Base class for all data manager result containers.""" + + model_input_keys: Optional[Dict[str, Any]] = None + rt: Optional[bool] = False + fallback_option: Optional[RealTimeFallbackOption] = None + + def __post_init__(self): + """Simple formatting""" + timeseries = getattr(self, "timeseries", None) + if timeseries is not None: + if isinstance(timeseries, (pd.Series, pd.DataFrame)): + timeseries.index.name = "datetime" + timeseries.index = to_datetime(timeseries.index) + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides additional fields for string representation. Override in subclasses.""" + return {} + + def is_empty(self) -> bool: + """Checks if the result container has no data. Override in subclasses if needed.""" + raise NotImplementedError("is_empty method must be implemented in subclasses.") + + def __repr__(self) -> str: + """Returns string representation with additional fields from subclass.""" + additional_fields = self._additional_repr_fields() + if additional_fields: + fields_str = ", ".join(f"{k}={v!r}" for k, v in additional_fields.items()) + return f"{self.__class__.__name__}({fields_str})" + return f"{self.__class__.__name__}()" + + def __setattr__(self, name, value): + """Validates inputs on attribute set.""" + all_hints = get_type_hints(self.__class__) + hint = all_hints.get(name) + if hint is not None: + check_type(value, hint) + super().__setattr__(name, value) + + +@dataclass +class _EquityResultsBase(Result): + """Base class for equity-related result containers.""" + + symbol: Optional[str] = None + + def __repr__(self): + return super().__repr__() + + +@dataclass +class DividendsResult(_EquityResultsBase): + """Contains dividend schedule or yield data for a date range.""" + timeseries: Optional[pd.Series] = None + dividend_type: Optional[DivType] = None + key: Optional[str] = None + undo_adjust: Optional[bool] = None + + @property + def daily_discrete_dividends(self) -> Optional[pd.Series]: + if self.dividend_type == DivType.DISCRETE: + return self.timeseries + return None + + @daily_discrete_dividends.setter + def daily_discrete_dividends(self, value: Optional[pd.Series]) -> None: + self.timeseries = value + + @property + def daily_continuous_dividends(self) -> Optional[pd.Series]: + if self.dividend_type == DivType.CONTINUOUS: + return self.timeseries + return None + + @daily_continuous_dividends.setter + def daily_continuous_dividends(self, value: Optional[pd.Series]) -> None: + self.timeseries = value + + + ## For schedule timeseries, this will be the actual schedule keys + model_input_keys: Optional[Dict[str, Any]] = None + + def __repr__(self) -> str: + return super().__repr__() + + def is_empty(self) -> bool: + """Checks if dividend data is missing or empty.""" + if self.dividend_type == DivType.DISCRETE: + return self.daily_discrete_dividends is None or self.daily_discrete_dividends.empty + elif self.dividend_type == DivType.CONTINUOUS: + return self.daily_continuous_dividends is None or self.daily_continuous_dividends.empty + return True + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides dividend-specific fields for string representation.""" + return { + "symbol": self.symbol, + "dividend_type": self.dividend_type, + "key": self.key, + "is_empty": self.is_empty(), + "undo_adjust": self.undo_adjust, + } + + def __setattr__(self, name, value): + + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "dividends" + + super().__setattr__(name, value) + +@dataclass +class RatesResult(Result): + """Contains risk-free rate data for a date range.""" + + symbol: Optional[str] = None + timeseries: Optional[pd.Series] = None + + @property + def daily_risk_free_rates(self) -> Optional[pd.Series]: + return self.timeseries + + @daily_risk_free_rates.setter + def daily_risk_free_rates(self, value: Optional[pd.Series]) -> None: + self.timeseries = value + + def is_empty(self) -> bool: + """Checks if rate data is missing or empty.""" + return self.timeseries is None or self.timeseries.empty + + def _additional_repr_fields(self): + """Provides rate-specific fields for string representation.""" + return { + "is_empty": self.is_empty(), + } + + def __repr__(self) -> str: + return super().__repr__() + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "r" + + super().__setattr__(name, value) + + +@dataclass +class ForwardResult(_EquityResultsBase): + """Contains forward price data (discrete or continuous dividend model).""" + + timeseries: Optional[pd.Series] = None + dividend_type: Optional[DivType] = None + key: Optional[str] = None + dividend_result: Optional[DividendsResult] = None + undo_adjust: Optional[bool] = True + + ## Dividend schedule or yield model input keys + ## Rates model input keys + model_input_keys: Optional[Dict[str, Any]] = None + + @property + def daily_discrete_forward(self) -> Optional[pd.Series]: + if self.dividend_type == DivType.DISCRETE: + return self.timeseries + return None + + @daily_discrete_forward.setter + def daily_discrete_forward(self, value: Optional[pd.Series]) -> None: + self.timeseries = value + + @property + def daily_continuous_forward(self) -> Optional[pd.Series]: + if self.dividend_type == DivType.CONTINUOUS: + return self.timeseries + return None + + @daily_continuous_forward.setter + def daily_continuous_forward(self, value: Optional[pd.Series]) -> None: + self.timeseries = value + + + def is_empty(self) -> bool: + """Checks if forward price data is missing or empty.""" + if self.dividend_type == DivType.DISCRETE: + return self.daily_discrete_forward is None or self.daily_discrete_forward.empty + elif self.dividend_type == DivType.CONTINUOUS: + return self.daily_continuous_forward is None or self.daily_continuous_forward.empty + return True + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides forward-specific fields for string representation.""" + return { + "symbol": self.symbol, + "is_empty": self.is_empty(), + "undo_adjust": self.undo_adjust, + "dividend_type": self.dividend_type, + "key": self.key, + } + + def __repr__(self) -> str: + return super().__repr__() + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "forward" + + super().__setattr__(name, value) + + +@dataclass +class SpotResult(_EquityResultsBase): + """Contains spot price data with optional split adjustment information.""" + + timeseries: Optional[pd.Series] = None + undo_adjust: Optional[bool] = None + key: Optional[str] = None + + ## For spot timeseries. This is nothing but an indicator of the source of spot data. + model_input_keys: Optional[Dict[str, Any]] = None + + @property + def daily_spot(self) -> Optional[pd.Series]: + return self.timeseries + + @daily_spot.setter + def daily_spot(self, value: Optional[pd.Series]) -> None: + self.timeseries = value + + def is_empty(self) -> bool: + return self.daily_spot is None or self.daily_spot.empty + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides spot-specific fields for string representation.""" + return { + "symbol": self.symbol, + "key": self.key, + "is_empty": self.is_empty(), + "undo_adjust": self.undo_adjust, + } + + def __repr__(self) -> str: + return super().__repr__() + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "spot" if self.undo_adjust else "spot_unadjusted" + + super().__setattr__(name, value) + + +@dataclass +class _OptionResultsBase(Result): + """Base class for option-related result containers.""" + + symbol: Optional[str] = None + strike: Optional[float] = None + expiration: Optional[DATE_HINT] = None + right: Optional[str] = None + model_price: Optional[ModelPrice] = ModelPrice.MIDPOINT + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides option-specific fields for string representation.""" + return { + "symbol": self.symbol, + "strike": self.strike, + "expiration": self.expiration, + "right": self.right, + "model_price": self.model_price, + } + + def __repr__(self) -> str: + """Delegates to base Result repr.""" + return super().__repr__() + +@dataclass +class _OptionModelResultsBase(_OptionResultsBase): + """Base class for option model result containers.""" + endpoint_source: Optional[OptionSpotEndpointSource] = None + market_model: Optional[OptionPricingModel] = None + vol_model: Optional[VolatilityModel] = None + dividend_type: Optional[DivType] = None + undo_adjust: Optional[bool] = None + + def __repr__(self) -> str: + """Delegates to base Result repr.""" + return super().__repr__() + + +@dataclass +class OptionSpotResult(_OptionResultsBase): + """Container for option spot price timeseries data.""" + + timeseries: Optional[pd.DataFrame] = None + key: Optional[str] = None + endpoint_source: Optional[OptionSpotEndpointSource] = None + + + ## For option spot timeseries, this will be the actual endpoint parameters + model_input_keys: Optional[Dict[str, Any]] = None + + @property + def daily_option_spot(self) -> Optional[pd.DataFrame]: + return self.timeseries + + @daily_option_spot.setter + def daily_option_spot(self, value: Optional[pd.DataFrame]) -> None: + self.timeseries = value + + @property + def price(self) -> pd.Series: + if self.rt: + return self.midpoint + + if not self.is_empty(): + if self.model_price == ModelPrice.CLOSE: + p = self.daily_option_spot.get("close") + elif self.model_price == ModelPrice.MIDPOINT: + p = self.daily_option_spot.get("midpoint") + elif self.model_price == ModelPrice.BID: + p = self.daily_option_spot.get("closebid") + elif self.model_price == ModelPrice.ASK: + p = self.daily_option_spot.get("closeask") + elif self.model_price == ModelPrice.OPEN: + p = self.daily_option_spot.get("open") + else: + p = self.daily_option_spot.get("midpoint") + else: + return pd.Series(name="price", index=pd.DatetimeIndex([]), dtype=float) + + if p is None: + raise ValueError(f"Requested model price '{self.model_price}' not found in option spot data. Available columns: {self.daily_option_spot.columns.tolist()}") + return p + + @property + def close(self) -> pd.Series: + if not self.is_empty(): + return self.daily_option_spot["close"] + else: + return pd.Series(name="close", index=pd.DatetimeIndex([]), dtype=float) + + @property + def midpoint(self) -> pd.Series: + if not self.is_empty(): + return self.daily_option_spot["midpoint"] + else: + return pd.Series(name="midpoint", index=pd.DatetimeIndex([]), dtype=float) + + def is_empty(self) -> bool: + """Checks if option spot data is missing or empty.""" + return self.daily_option_spot is None or self.daily_option_spot.empty + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides metadata on data presence.""" + return { + "symbol": self.symbol, + "strike": self.strike, + "expiration": self.expiration, + "right": self.right, + "key": self.key, + "is_empty": self.is_empty(), + "endpoint_source": self.endpoint_source, + } + + def __repr__(self) -> str: + """Delegates to base Result repr.""" + return super().__repr__() + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "option_spot" + + super().__setattr__(name, value) + + +@dataclass +class VolatilityResult(_OptionModelResultsBase): + """Contains volatility surface data.""" + + timeseries: Optional[pd.Series] = None + key: Optional[str] = None + model_input_keys: Optional[Dict[str, Any]] = None + + def is_empty(self) -> bool: + """Checks if volatility data is missing or empty.""" + return self.timeseries is None or self.timeseries.empty + + def _additional_repr_fields(self) -> Dict[str, Any]: + """Provides volatility-specific fields for string representation.""" + return { + "symbol": self.symbol, + "expiration": self.expiration, + "right": self.right, + "strike": self.strike, + "vol_model": self.vol_model, + "endpoint_source": self.endpoint_source, + "market_model": self.market_model, + "dividend_type": self.dividend_type, + "key": self.key, + "is_empty": self.is_empty(), + } + + def __repr__(self) -> str: + return super().__repr__() + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "iv" + + super().__setattr__(name, value) + + +@dataclass +class GreekResultSet(_OptionModelResultsBase): + key: Optional[str] = None + timeseries: Optional[pd.DataFrame] = None + + def is_empty(self) -> bool: + return self.timeseries is None or self.timeseries.empty + + def _additional_repr_fields(self) -> Dict[str, Any]: + super_additional = super()._additional_repr_fields() + return { + **super_additional, + "Available Greeks": [g for g in AVAILABLE_GREEKS if self.timeseries is not None and g in self.timeseries.columns], + "empty": self.is_empty(), + } + + def __repr__(self): + return super().__repr__() + + @property + def delta(self) -> Optional[pd.Series]: + if self.timeseries is not None and GreekType.DELTA.value in self.timeseries.columns: + return self.timeseries[GreekType.DELTA.value] + return None + + @property + def gamma(self) -> Optional[pd.Series]: + if self.timeseries is not None and GreekType.GAMMA.value in self.timeseries.columns: + return self.timeseries[GreekType.GAMMA.value] + return None + + @property + def theta(self) -> Optional[pd.Series]: + if self.timeseries is not None and GreekType.THETA.value in self.timeseries.columns: + return self.timeseries[GreekType.THETA.value] + return None + + @property + def vega(self) -> Optional[pd.Series]: + if self.timeseries is not None and GreekType.VEGA.value in self.timeseries.columns: + return self.timeseries[GreekType.VEGA.value] + return None + + @property + def rho(self) -> Optional[pd.Series]: + if self.timeseries is not None and GreekType.RHO.value in self.timeseries.columns: + return self.timeseries[GreekType.RHO.value] + return None + + @property + def volga(self) -> Optional[pd.Series]: + if self.timeseries is not None and GreekType.VOLGA.value in self.timeseries.columns: + return self.timeseries[GreekType.VOLGA.value] + return None + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "greeks" + + super().__setattr__(name, value) + + +@dataclass +class TheoreticalPriceResult(_OptionModelResultsBase): + timeseries: Optional[pd.Series] = None + + def is_empty(self) -> bool: + return self.timeseries is None or self.timeseries.empty + + def __repr__(self) -> str: + return super().__repr__() + + def __setattr__(self, name, value): + ## Intercept dataframe/series, and add name attribute if missing. Only add name for series. + ## Not ideal to do it here, but easier than finding all places where timeseries is set. + if name == "timeseries" and value is not None: + if isinstance(value, (pd.Series, pd.DataFrame)): + value.index.name = "datetime" + if isinstance(value, pd.Series): + if value.name is None: + value.name = "theoretical_price" + + super().__setattr__(name, value) + + + +@dataclass +class ScenariosResult(_OptionModelResultsBase): + grid: Optional[pd.DataFrame] = None + spot_scenarios: List[float] = field(default_factory=lambda: []) + vol_scenarios: List[float] = field(default_factory=lambda: []) + as_of: Optional[DATE_HINT] = None + + def is_empty(self) -> bool: + return self.grid is None or self.grid.empty + + def _additional_repr_fields(self): + return { + "symbol": self.symbol, + "expiration": self.expiration, + "right": self.right, + "strike": self.strike, + "market_model": self.market_model, + "dividend_type": self.dividend_type, + "num_spot_scenarios": len(self.spot_scenarios), + "num_vol_scenarios": len(self.vol_scenarios), + "is_empty": self.is_empty(), + } + + def __repr__(self) -> str: + return super().__repr__() +@dataclass +class ModelResultPack(Result): + """ + A container for various model result types. + """ + + ## Main Results + spot: Optional[SpotResult] = None + forward: Optional[ForwardResult] = None + dividend: Optional[DividendsResult] = None + rates: Optional[RatesResult] = None + option_spot: Optional[OptionSpotResult] = None + vol: Optional[VolatilityResult] = None + greek: Optional[GreekResultSet] = None + + ## Guiding Enums + series_id: Optional[SeriesId] = None + dividend_type: Optional[DivType] = None + undo_adjust: bool = True + endpoint_source: Optional[OptionSpotEndpointSource] = None + price: Optional[ModelPrice] = None + rt: Optional[bool] = False + on_date: Optional[bool] = False + + ## Diagnostic Info + time_to_load: Optional[Dict[str, float]] = None + + def _additional_repr_fields(self): + """Provides model-specific fields for string representation.""" + return { + "symbol": self.spot.symbol if self.spot else None, + "strike": self.option_spot.strike if self.option_spot else None, + "expiration": self.option_spot.expiration if self.option_spot else None, + "right": self.option_spot.right if self.option_spot else None, + "series_id": self.series_id, + "dividend_type": self.dividend_type, + "undo_adjust": self.undo_adjust, + "num_empty": sum( + 1 + for result in [ + self.spot, + self.forward, + self.dividend, + self.rates, + self.option_spot, + self.vol, + self.greek, + ] + if result is None or result.is_empty() + ), + } + + def __repr__(self) -> str: + return super().__repr__() + + def list_all_loaded(self) -> Dict[str, bool]: + return { + "spot": self.spot is not None and not self.spot.is_empty(), + "forward": self.forward is not None and not self.forward.is_empty(), + "dividend": self.dividend is not None and not self.dividend.is_empty(), + "rates": self.rates is not None and not self.rates.is_empty(), + "option_spot": self.option_spot is not None and not self.option_spot.is_empty(), + "vol": self.vol is not None and not self.vol.is_empty(), + "greek": self.greek is not None and not self.greek.is_empty(), + } + + def any_loaded(self) -> bool: + return any( + [ + self.spot is not None and not self.spot.is_empty(), + self.forward is not None and not self.forward.is_empty(), + self.dividend is not None and not self.dividend.is_empty(), + self.rates is not None and not self.rates.is_empty(), + self.option_spot is not None and not self.option_spot.is_empty(), + self.vol is not None and not self.vol.is_empty(), + self.greek is not None and not self.greek.is_empty(), + ] + ) + + def all_loaded(self) -> bool: + return all( + [ + self.spot is not None and not self.spot.is_empty(), + self.forward is not None and not self.forward.is_empty(), + self.dividend is not None and not self.dividend.is_empty(), + self.rates is not None and not self.rates.is_empty(), + self.option_spot is not None and not self.option_spot.is_empty(), + self.vol is not None and not self.vol.is_empty(), + self.greek is not None and not self.greek.is_empty(), + ] + ) + + # def all_passed_loaded(self, requested: List[str]) -> bool: + # mapping = { + # "spot": self.load_spot, + # "forward": self.load_forward, + # "dividend": self.load_dividend, + # "rates": self.load_rates, + # "option_spot": self.load_option_spot, + # "vol": self.load_vol, + # } + # return all([mapping[req] for req in requested if req in mapping]) + diff --git a/trade/datamanager/spot.py b/trade/datamanager/spot.py new file mode 100644 index 0000000..d23f5e6 --- /dev/null +++ b/trade/datamanager/spot.py @@ -0,0 +1,287 @@ +"""Spot price data management for options pricing with split adjustment support. + +This module provides the SpotDataManager class for retrieving spot (or split-adjusted +chain_spot) prices for equity symbols. Implements singleton pattern per symbol to +avoid redundant timeseries loading. + +Typical usage: + >>> spot_mgr = SpotDataManager("AAPL") + >>> result = spot_mgr.get_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... undo_adjust=True + ... ) + >>> prices = result.daily_spot +""" + +from datetime import datetime +from typing import Any, ClassVar, Optional, Union +from trade.datamanager.utils.date import is_available_on_date +from trade.helpers.Logging import setup_logger +import pandas as pd +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager.result import SpotResult +from trade.datamanager.vars import get_times_series, load_name +from trade.helpers.helper import change_to_last_busday, to_datetime +from trade.datamanager._enums import RealTimeFallbackOption, SeriesId +from trade.datamanager.utils.logging import get_logging_level +from trade.datamanager.utils.data_structure import _data_structure_sanitize + + +logger = setup_logger("trade.datamanager.spot", stream_log_level=get_logging_level()) +TS = get_times_series() # Load market timeseries data on module import to avoid circular imports +class SpotDataManager(BaseDataManager): + """Manages spot price retrieval for a specific symbol with split adjustment support. + + Provides access to spot prices (unadjusted) or chain_spot prices (split-adjusted) + from the global MarketTimeseries cache. Implements singleton pattern per symbol + to ensure efficient data access. + + Attributes: + CACHE_NAME: Class-level cache identifier for this manager type. + DEFAULT_SERIES_ID: Default historical series identifier. + INSTANCES: Class-level cache of manager instances per symbol. + symbol: The equity ticker symbol this manager handles. + + Examples: + >>> # Singleton access - same instance returned for same symbol + >>> spot_mgr1 = SpotDataManager("AAPL") + >>> spot_mgr2 = SpotDataManager("AAPL") + >>> assert spot_mgr1 is spot_mgr2 + + >>> # Get split-adjusted prices (chain_spot) + >>> result = spot_mgr1.get_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... undo_adjust=True + ... ) + >>> chain_spot = result.daily_spot + + >>> # Get unadjusted prices (spot) + >>> result = spot_mgr1.get_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... undo_adjust=False + ... ) + >>> spot = result.daily_spot + + >>> # Get price at specific datetime + >>> at_time_result = spot_mgr1.get_at_time("2025-01-15") + >>> price = at_time_result.close + """ + + CACHE_NAME: ClassVar[str] = "spot_data_manager" + DEFAULT_SERIES_ID: ClassVar["SeriesId"] = SeriesId.HIST + INSTANCES = {} + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + + def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> "SpotDataManager": + """Returns cached instance for symbol, creating new one if needed. + + Implements singleton pattern per symbol to ensure timeseries are loaded only once. + Automatically loads market timeseries data on first instantiation. + + Args: + symbol: Equity ticker symbol (e.g., "AAPL", "MSFT"). + *args: Additional positional arguments passed to __init__. + **kwargs: Additional keyword arguments passed to __init__. + + Returns: + Singleton SpotDataManager instance for the given symbol. + + Examples: + >>> mgr1 = SpotDataManager("AAPL") + >>> mgr2 = SpotDataManager("AAPL") + >>> assert mgr1 is mgr2 # Same instance + """ + if symbol not in cls.INSTANCES: + instance = super(SpotDataManager, cls).__new__(cls) + cls.INSTANCES[symbol] = instance + return cls.INSTANCES[symbol] + + def __init__( + self, symbol: str, *, enable_namespacing: bool = False + ) -> None: + """Initializes manager once per symbol instance. + + Sets up the data manager for the symbol. Only executes initialization logic + on first instantiation due to singleton pattern. + + Args: + symbol: Equity ticker symbol. + cache_spec: Optional cache configuration. Uses default if None. + enable_namespacing: If True, enables namespace isolation in cache keys. + + Examples: + >>> mgr = SpotDataManager("AAPL") + >>> mgr = SpotDataManager("AAPL", cache_spec=CacheSpec(expire_days=30)) + """ + if getattr(self, "_initialized", False): + return + self._initialized = True + super().__init__(enable_namespacing=enable_namespacing, symbol=symbol) + self.symbol = symbol + + def get_spot_timeseries( + self, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + undo_adjust: bool = True, + ) -> SpotResult: + """Returns spot or chain_spot price series for date range from MarketTimeseries. + + Retrieves closing prices from the global MarketTimeseries cache. Returns either + split-adjusted (chain_spot) or unadjusted (spot) prices based on undo_adjust flag. + + Args: + start_date: Start of date range (YYYY-MM-DD string or datetime). + end_date: End of date range (YYYY-MM-DD string or datetime). + undo_adjust: If True, returns split-adjusted chain_spot prices. + If False, returns unadjusted spot prices. + + Returns: + SpotResult containing daily_spot Series indexed by datetime, plus metadata + (undo_adjust flag and cache key). + + Examples: + >>> spot_mgr = SpotDataManager("AAPL") + >>> # Get split-adjusted prices (recommended for backtesting) + >>> result = spot_mgr.get_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... undo_adjust=True + ... ) + >>> chain_spot = result.daily_spot + >>> print(chain_spot.head()) + datetime + 2025-01-02 155.32 + 2025-01-03 156.01 + ... + + >>> # Get unadjusted prices (for real-time pricing) + >>> result = spot_mgr.get_spot_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... undo_adjust=False + ... ) + >>> spot = result.daily_spot + + Notes: + - chain_spot: Split-adjusted prices (use with undo_adjust=True dividends) + - spot: Unadjusted prices (use with undo_adjust=False dividends) + - Data loaded directly from global TS cache (no additional caching) + - Automatically filters to business days (excludes weekends/holidays) + """ + ## Load first + load_name(self.symbol) + + if undo_adjust: + spot_series = TS._get_chain_spot_timeseries(sym=self.symbol, start=start_date, end=end_date)["close"] + else: + spot_series = TS._get_spot_timeseries(sym=self.symbol, start=start_date, end=end_date)["close"] + + spot_series = _data_structure_sanitize( + spot_series, + start=start_date, + end=end_date, + source_name=f"{'chain_spot' if undo_adjust else 'spot'} timeseries for {self.symbol} from MarketTimeseries cache", + ) + result = SpotResult() + key = None # No caching key for now + result.daily_spot = spot_series + result.undo_adjust = undo_adjust + result.key = key + result.symbol = self.symbol + + return result + + def get_at_time( + self, + date: Union[datetime, str], + undo_adjust: bool = True, + fallback_option: Optional[RealTimeFallbackOption] = None, + ) -> SpotResult: + """Returns spot data at a specific datetime from MarketTimeseries. + + Retrieves comprehensive market data (OHLCV + other fields) for a specific date + or datetime. Useful for point-in-time lookups. + + Args: + date: Target date or datetime (YYYY-MM-DD string or datetime object). + + Returns: + AtIndexResult containing OHLCV data and other market fields at the + specified datetime. + + Examples: + >>> spot_mgr = SpotDataManager("AAPL") + >>> result = spot_mgr.get_at_time("2025-01-15") + >>> print(f"Close: ${result.close:.2f}") + Close: $156.45 + >>> print(f"Volume: {result.volume:,.0f}") + Volume: 45,123,000 + + >>> # Using datetime object + >>> from datetime import datetime + >>> result = spot_mgr.get_at_time(datetime(2025, 1, 15)) + >>> print(f"Open: ${result.open:.2f}, High: ${result.high:.2f}") + Open: $155.20, High: $157.80 + + Notes: + - Returns data as of market close for the specified date + - Delegates to global TS.get_at_index method + - Result includes open, high, low, close, volume, and other fields + """ + fallback_option = fallback_option or self.CONFIG.real_time_fallback_option + if not is_available_on_date(to_datetime(date).date()): + logger.warning( + f"Requested date {date} is not a business day or is a US holiday. Resorting to fallback option `{fallback_option}`." + ) + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"Date {date} is not available for risk-free rate data.") + + if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## Using only change_to_last_busday assumes input date is not business day or is holiday + ## Which the function would roll back + ## But there's a possibility input date is today's date but before market open + ## In that case we need to move back one more business day + date = change_to_last_busday(date - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + else: + raise ValueError(f"Unsupported fallback option: {fallback_option}") + + ## Load first + load_name(self.symbol) + res = TS.get_at_index(sym=self.symbol, index=date) + container = SpotResult() + container.symbol = self.symbol + container.rt = True + container.timeseries = res.chain_spot if undo_adjust else res.spot + container.timeseries = container.timeseries.to_frame().T["close"] + container.undo_adjust = undo_adjust + container.timeseries.index = pd.to_datetime(container.timeseries.index, format="%Y-%m-%d") + container.fallback_option = fallback_option + return container + + def rt( + self, + fallback_option: Optional[RealTimeFallbackOption] = None, + undo_adjust: bool = True, + ) -> SpotResult: + """Returns the most recent spot price for the symbol. + + Retrieves the latest available spot price from the MarketTimeseries cache. + Useful for real-time pricing scenarios. + + Returns: + Most recent spot price as a float. + Examples: + >>> spot_mgr = SpotDataManager("AAPL") + >>> latest_price = spot_mgr.rt() + >>> print(f"Latest AAPL Price: ${latest_price:.2f}") + Latest AAPL Price: $158.23 + """ + + date = datetime.now() + at_index_result = self.get_at_time(date=date, undo_adjust=undo_adjust) + return at_index_result diff --git a/trade/datamanager/theo.py b/trade/datamanager/theo.py new file mode 100644 index 0000000..a27c4bf --- /dev/null +++ b/trade/datamanager/theo.py @@ -0,0 +1,961 @@ +"""Theoretical option pricing module for computing fair values and scenario analysis. + +This module provides functions for calculating theoretical option prices using various +pricing models (Black-Scholes-Merton, Cox-Ross-Rubinstein binomial) and performing +scenario analysis across different spot and volatility levels. It handles the complete +workflow including data loading, model selection, and result formatting. + +Key Features: + - Multiple pricing models: BSM, CRR binomial + - Support for American and European exercise styles + - Discrete and continuous dividend treatments + - Automatic data loading + - Scenario analysis (spot and volatility stress testing) + - P&L analysis capabilities + +Typical Usage: + >>> from trade.datamanager.theo import get_option_theoretical_price, calculate_scenarios + >>> from trade.optionlib.config.types import DivType + >>> from trade.datamanager._enums import OptionPricingModel + >>> + >>> # Get theoretical prices for an option over time + >>> result = get_option_theoretical_price( + ... symbol="AAPL", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... market_model=OptionPricingModel.BSM, + ... dividend_type=DivType.DISCRETE + ... ) + >>> print(result.timeseries.head()) + >>> + >>> # Run scenario analysis for risk management + >>> scenarios = calculate_scenarios( + ... symbol="AAPL", + ... as_of="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... spot_scenarios=[0.9, 0.95, 1.0, 1.05, 1.1], + ... vol_scenarios=[-0.05, 0.0, 0.05], + ... return_pnl=True + ... ) + >>> print(scenarios.grid) +""" +from datetime import datetime +from itertools import product +import pandas as pd +from typing import Optional, Literal, Dict, List +from trade.datamanager.utils.model import ( + LoadRequest, + _load_model_data_timeseries, + DivType, + VolatilityModel, + OptionPricingModel, +) +from trade.helpers.helper import time_distance_helper +from trade.datamanager.config import OptionDataConfig +from trade.datamanager.result import ( + VolatilityResult, + ForwardResult, + RatesResult, + OptionSpotResult, + SpotResult, + DividendsResult, + TheoreticalPriceResult, + ScenariosResult, +) +from trade.datamanager._enums import ( + OptionSpotEndpointSource, + ModelPrice, +) +from trade.datamanager.utils.model import _adjust_div_yield_for_spot_shock +from trade.datamanager.utils.date import DATE_HINT +from trade.optionlib.assets.dividend import ( + vectorized_discrete_pv, + get_vectorized_continuous_dividends, + vector_convert_to_time_frac, +) +from trade.datamanager.utils.date import sync_date_index, to_datetime +from trade.helpers.Logging import setup_logger +from trade.optionlib.pricing.binomial import vector_crr_binomial_pricing +from trade.optionlib.pricing.black_scholes import black_scholes_vectorized +from trade.optionlib.assets.forward import vectorized_forward_continuous, vectorized_forward_discrete +from trade.datamanager.utils.logging import get_logging_level, register_to_factor_list +from trade.datamanager.vars import DEFAULT_SCENARIOS, DEFAULT_VOL_SCENARIOS + +logger = setup_logger("trade.datamanager.theo", stream_log_level=get_logging_level()) +register_to_factor_list("trade.datamanager.theo") +CONFIG = OptionDataConfig() + + +def _create_load_request( + ## Requied parameters to ensure correct data is loaded + symbol: str, + expiration: DATE_HINT, + strike: float, + right: str, + dividend_type: DivType, + market_model: OptionPricingModel, + endpoint_source: OptionSpotEndpointSource, + model_price: ModelPrice, + is_scenario_load: bool = False, + *, + ## Optional pre-loaded data. If not provided, will be loaded. + start_date: Optional[DATE_HINT] = None, + end_date: Optional[DATE_HINT] = None, + as_of: Optional[DATE_HINT] = None, + rt: Optional[bool] = False, + s: Optional[SpotResult] = None, + r: Optional[RatesResult] = None, + f: Optional[ForwardResult] = None, + d: Optional[DividendsResult] = None, + vol: Optional[VolatilityResult] = None, + option_spot: Optional[OptionSpotResult] = None, + undo_adjust: bool = True, +) -> LoadRequest: + """Create a LoadRequest specifying which market data to load for theoretical pricing. + + Internal utility that determines which data sources need to be loaded based on: + 1. Which data is already provided (pre-loaded) + 2. Which pricing model is being used (BSM needs forwards, binomial needs spot) + 3. Whether this is a scenario load (requires additional data) + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + symbol: Ticker symbol for the underlying asset. + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + market_model: Pricing model (BSM or BINOMIAL). + endpoint_source: Option data source (ORATS, HIST, QUOTE). + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). + is_scenario_load: If True, loads option_spot for base price comparison. + s: Optional pre-loaded spot data. If None, will be loaded. + r: Optional pre-loaded rates data. If None, will be loaded. + f: Optional pre-loaded forward data. If None, will be loaded (BSM only). + d: Optional pre-loaded dividend data. If None, will be loaded. + vol: Optional pre-loaded volatility data. If None, will be loaded. + option_spot: Optional pre-loaded option market prices. If None, loaded for scenarios. + undo_adjust: If True, uses split-adjusted prices. + + Returns: + LoadRequest object with flags indicating which data sources to load. + + Examples: + >>> # Internal usage - creates request for theoretical pricing + >>> request = _create_load_request( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... symbol="AAPL", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE, + ... market_model=OptionPricingModel.BSM, + ... endpoint_source=OptionSpotEndpointSource.HIST, + ... model_price=ModelPrice.CLOSE + ... ) + >>> # request.load_spot = True (BSM needs spot for forward calc) + >>> # request.load_vol = True (no vol provided) + """ + if is_scenario_load: + ## For scenario loads, always load all data to ensure completeness. + load_spot = s is None + load_vol = vol is None + load_dividend = d is None + load_rates = r is None + option_spot = option_spot is None + load_forward = False ## Not needed for scenario load + else: + ## For regular loads, determine based on provided data and model needs. + load_spot = (s is None) and (market_model == OptionPricingModel.BINOMIAL) + load_vol = vol is None + load_dividend = d is None + load_rates = r is None + option_spot = False ## Not needed for greek calculation + load_forward = (market_model == OptionPricingModel.BSM) and (f is None) + + req = LoadRequest( + symbol=symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + vol_model=VolatilityModel.MARKET, + model_price=model_price, + market_model=market_model, + + ## Load spot only if missing. + load_spot=load_spot, + + ## Load forward only if missing and using BSM model. Binomial uses spot price. + load_forward=load_forward, + load_vol=load_vol, + load_dividend=load_dividend, + load_rates=load_rates, + + ## Not needed for greek calculation + load_option_spot=option_spot, + undo_adjust=undo_adjust, + + ## Real-time/Date flag + rt=rt, + as_of=as_of, + ) + return req + + +def get_option_theoretical_price( + symbol: str, + strike: float, + expiration: DATE_HINT, + right: Literal["c", "p"], + *, + start_date: Optional[DATE_HINT] = None, + end_date: Optional[DATE_HINT] = None, + as_of: Optional[DATE_HINT] = None, + market_model: Optional[OptionPricingModel] = None, + endpoint_source: OptionSpotEndpointSource = None, + dividend_type: Optional[DivType] = None, + vol: Optional[VolatilityResult] = None, + model_price: Optional[ModelPrice] = None, + spot: Optional[SpotResult] = None, + f: Optional[ForwardResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + rt: Optional[bool] = False, +) -> TheoreticalPriceResult: + """Calculate theoretical option prices over a date range using specified pricing model. + + Computes fair value option prices for each business day in [start_date, end_date] + using either BSM or binomial pricing models. Automatically loads required market + data (spot, volatility, rates, dividends) if not provided. + + Args: + symbol: Ticker symbol for the underlying asset (e.g., "AAPL", "MSFT"). + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + as_of: Specific date for single-date pricing (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + expiration: Option expiration date (YYYY-MM-DD string or datetime). + right: Option type ('c' for call, 'p' for put). + market_model: OptionPricingModel.BSM or BINOMIAL. Defaults to CONFIG setting. + endpoint_source: Option data source for volatility (ORATS, HIST, QUOTE). + Defaults to CONFIG setting. + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to CONFIG setting. + vol: Optional pre-computed implied volatilities. If None, loads automatically. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). Defaults to CONFIG setting. + spot: Optional pre-computed spot prices. If None, loads automatically. + f: Optional pre-computed forward prices. If None, loads automatically (BSM only). + r: Optional pre-computed risk-free rates. If None, loads automatically. + d: Optional pre-computed dividend data. If None, loads automatically. + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of time steps for binomial tree. Defaults to CONFIG.n_steps. + as_of: Specific date for single-date pricing (YYYY-MM-DD string or datetime). = None, + rt: If True, prices as of current real-time data. + + Returns: + TheoreticalPriceResult containing daily theoretical prices as Series with + DatetimeIndex, plus model metadata. + + Examples: + >>> # Basic usage with BSM model + >>> result = get_option_theoretical_price( + ... symbol="AAPL", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... market_model=OptionPricingModel.BSM, + ... dividend_type=DivType.DISCRETE + ... ) + >>> print(result.timeseries.head()) + + >>> # American option with binomial model + >>> result = get_option_theoretical_price( + ... symbol="AAPL", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="p", + ... market_model=OptionPricingModel.BINOMIAL, + ... n_steps=200 + ... ) + + >>> # Provide pre-computed volatility + >>> from trade.datamanager.vol import VolDataManager + >>> vol_mgr = VolDataManager("AAPL") + >>> vol_result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + >>> theo_result = get_option_theoretical_price( + ... symbol="AAPL", + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... vol=vol_result + ... ) + """ + + if not as_of and not rt and (not start_date or not end_date): + raise ValueError("Either 'as_of', rt=True, or both 'start_date' and 'end_date' must be provided.") + + market_model = market_model or CONFIG.option_model + endpoint_source = endpoint_source or CONFIG.option_spot_endpoint_source + dividend_type = dividend_type or CONFIG.dividend_type + vol_model = CONFIG.volatility_model + model_price = model_price or CONFIG.model_price + n_steps = n_steps or CONFIG.n_steps + result = TheoreticalPriceResult() + result.dividend_type = dividend_type + result.market_model = market_model + result.model_price = model_price + result.vol_model = vol_model + result.endpoint_source = endpoint_source + result.expiration = to_datetime(expiration) + result.right = right + result.strike = strike + result.symbol = symbol + result.rt = rt + result.undo_adjust = undo_adjust + + # Create load request to determine which data to load + load_request = _create_load_request( + symbol=symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + market_model=market_model, + endpoint_source=endpoint_source, + model_price=model_price, + s=spot, + f=f, + r=r, + vol=vol, + is_scenario_load=False, + rt=rt, + as_of=as_of, + ) + + # Load required market data + packet = _load_model_data_timeseries(load_request) + + # Extract time series data, using provided data if available + s, r, vol, d, f = ( + packet.spot.timeseries + if not packet.spot.is_empty() + else spot.timeseries + if spot is not None + else pd.Series(dtype=float), + packet.rates.timeseries + if not packet.rates.is_empty() + else r.timeseries + if r is not None + else pd.Series(dtype=float), + packet.vol.timeseries + if not packet.vol.is_empty() + else vol.timeseries + if vol is not None + else pd.Series(dtype=float), + packet.dividend.timeseries + if not packet.dividend.is_empty() + else d.timeseries + if d is not None + else pd.Series(dtype=float), + packet.forward.timeseries + if not packet.forward.is_empty() + else f.timeseries + if f is not None + else pd.Series(dtype=float), + ) + + # Use loaded data to calculate theoretical prices + if market_model == OptionPricingModel.BINOMIAL: + s, vol, r, d = sync_date_index(s, vol, r, d) + t = time_distance_helper(start=s.index, end=[expiration] * len(s)) + if dividend_type == DivType.DISCRETE: + discrete = vector_convert_to_time_frac( + schedules=d.values, + valuation_dates=d.index, + end_dates=[to_datetime(expiration)] * len(s), + ) + dividend_yield = [0.0] * len(s) + else: + discrete = [()] * len(s) + dividend_yield = d.values + + prices = vector_crr_binomial_pricing( + K=[strike] * len(s), + T=t, + sigma=vol.values, + r=r.values, + N=[n_steps] * len(s), + S0=s.values, + right=[right] * len(s), + american=[True] * len(s), + dividend_yield=dividend_yield, + dividends=discrete, + dividend_type=[dividend_type.value] * len(s), + ) + result.timeseries = pd.Series(data=prices, index=s.index, name="theoretical_price", dtype=float) + return result + + elif market_model == OptionPricingModel.BSM: + f, vol, r, d = ( + packet.forward.timeseries, + packet.vol.timeseries, + packet.rates.timeseries, + packet.dividend.timeseries, + ) + f, vol, r, d = sync_date_index(f, vol, r, d) + t = time_distance_helper(start=f.index, end=[expiration] * len(f)) + prices = black_scholes_vectorized( + F=f.values, + K=[strike] * len(f), + T=t, + r=r.values, + sigma=vol.values, + option_type=[right] * len(f), + ) + result.timeseries = pd.Series(data=prices, index=f.index, name="theoretical_price", dtype=float) + return result + + +def _calculate_binomial_scenarios( + base_prices: pd.Series, + s: pd.Series, + strike: float, + expiration: DATE_HINT, + right: Literal["c", "p"], + vol: pd.Series, + r: pd.Series, + dividend_type: DivType, + dividends: pd.Series, + spot_scenarios: List[float] = None, + vol_scenarios: List[float] = None, + return_pnl: bool = False, + return_pnl_in_pct: bool = False, + n_steps: int = None, + prettify_columns: bool = False, +) -> pd.DataFrame: + """Calculate option price scenarios using Cox-Ross-Rubinstein binomial model. + + Internal function that computes option prices across a grid of spot and volatility + scenarios. Spot scenarios are multiplicative (e.g., 0.9 = 10% down, 1.1 = 10% up). + Volatility scenarios are additive (e.g., 0.05 = +5% vol, -0.05 = -5% vol). + + Args: + base_prices: Current market prices of the option (single-date Series). + s: Current spot prices (single-date Series). + strike: Strike price of the option. + expiration: Option expiration date (YYYY-MM-DD string or datetime). + right: Option type ('c' for call, 'p' for put). + vol: Current implied volatilities (single-date Series). + r: Risk-free interest rates (single-date Series). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + dividends: Dividend data (schedules for DISCRETE, yields for CONTINUOUS). + spot_scenarios: List of spot price multipliers. E.g., [0.9, 1.0, 1.1] tests + spot -10%, unchanged, +10%. Defaults to [1.0]. + vol_scenarios: List of volatility adjustments. E.g., [-0.05, 0.0, 0.05] tests + vol -5%, unchanged, +5%. Defaults to [0.0]. + return_pnl: If True, returns P&L relative to base_prices instead of absolute prices. + return_pnl_in_pct: If True (with return_pnl=True), returns P&L as percentage. + n_steps: Number of time steps in binomial tree. + prettify_columns: If True, formats column/index labels for display. + + Returns: + DataFrame with volatility scenarios as rows, spot scenarios as columns, and + option prices (or P&L) as values. + + Raises: + AssertionError: If neither spot_scenarios nor vol_scenarios provided. + AssertionError: If input series contain more than one date. + + Examples: + >>> # Internal usage - calculate scenario grid + >>> scenarios_df = _calculate_binomial_scenarios( + ... base_prices=pd.Series([10.5]), + ... s=pd.Series([150.0]), + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... vol=pd.Series([0.25]), + ... r=pd.Series([0.05]), + ... dividend_type=DivType.DISCRETE, + ... dividends=pd.Series([Schedule()]), + ... spot_scenarios=[0.9, 0.95, 1.0, 1.05, 1.1], + ... vol_scenarios=[-0.05, 0.0, 0.05], + ... n_steps=100, + ... prettify_columns=True + ... ) + """ + assert any([spot_scenarios, vol_scenarios]), "At least one of spot_scenarios or vol_scenarios must be provided." + assert len(vol.index) == 1, "Spot scenarios calculation only supports single-date series." + + ## Default scenarios + if spot_scenarios is None: + spot_scenarios = [1.0] + if vol_scenarios is None: + vol_scenarios = [0.0] + + ## Sync all data to same index + s, vol, r, dividends, base_prices = sync_date_index(s, vol, r, dividends, base_prices) + scenario_prices: Dict[str, pd.Series] = {} + + ## Define pricing function for reuse + def price_func( + scenario_spot: pd.Series, + scenario_vol: pd.Series, + expiration: DATE_HINT, + right: Literal["c", "p"], + strike: float, + dividend_type: DivType, + dividends: pd.Series, + n_steps: int, + r: pd.Series, + ) -> pd.Series: + t = time_distance_helper(start=scenario_spot.index, end=[expiration] * len(scenario_spot)) + if dividend_type == DivType.DISCRETE: + discrete = vector_convert_to_time_frac( + schedules=dividends.values, + valuation_dates=scenario_spot.index, + end_dates=[to_datetime(expiration)] * len(scenario_spot), + ) + dividend_yield = [0.0] * len(scenario_spot) + else: + discrete = [()] * len(scenario_spot) + dividend_yield = dividends.values + + prices = vector_crr_binomial_pricing( + K=[strike] * len(scenario_spot), + T=t, + sigma=scenario_vol.values, + r=r.values, + N=[n_steps] * len(scenario_spot), + S0=scenario_spot.values, + right=[right] * len(scenario_spot), + american=[True] * len(scenario_spot), + dividend_yield=dividend_yield, + dividends=discrete, + dividend_type=[dividend_type.value] * len(scenario_spot), + ) + return pd.Series(data=prices, index=scenario_spot.index, name="theoretical_price", dtype=float) + + ## Calculate prices for each scenario + scenarios = list(product(spot_scenarios, vol_scenarios)) + for spot_mult, vol_add in scenarios: + scenario_spot = s * spot_mult + scenario_vol = vol + vol_add + if dividend_type == DivType.CONTINUOUS: + adjusted_dividends = _adjust_div_yield_for_spot_shock(spot_mult, dividends) + else: + adjusted_dividends = dividends + + prices = price_func( + scenario_spot, scenario_vol, expiration, right, strike, dividend_type, adjusted_dividends, n_steps, r + ) + prices = prices[0] + if return_pnl: + prices = prices - base_prices[0] + if return_pnl_in_pct: + prices = prices / base_prices[0] + scenario_prices.setdefault(spot_mult, []).append(prices) + + df = pd.DataFrame(scenario_prices, index=vol_scenarios) + if prettify_columns: + df.columns = [f"Spot x{col:.2f}" for col in df.columns] + df.index = [f"Vol {'+' if idx > 0 else ''}{idx:.2%}" for idx in df.index] + return df + + +def _calculate_bsm_scenarios( + base_prices: pd.Series, + s: pd.Series, + strike: float, + expiration: DATE_HINT, + right: Literal["c", "p"], + vol: pd.Series, + r: pd.Series, + dividend_type: DivType, + pv_divs: pd.Series = None, + q_factor: pd.Series = None, + spot_scenarios: List[float] = None, + vol_scenarios: List[float] = None, + return_pnl: bool = False, + return_pnl_in_pct: bool = False, + prettify_columns: bool = False, +) -> pd.DataFrame: + """Calculate option price scenarios using Black-Scholes-Merton model. + + Internal function that computes European-style option prices across a grid of spot + and volatility scenarios. Spot scenarios are multiplicative (e.g., 0.9 = 10% down, + 1.1 = 10% up). Volatility scenarios are additive (e.g., 0.05 = +5% vol, -0.05 = -5% vol). + + Args: + base_prices: Current market prices of the option (single-date Series). + s: Current spot prices (single-date Series). + strike: Strike price of the option. + expiration: Option expiration date (YYYY-MM-DD string or datetime). + right: Option type ('c' for call, 'p' for put). + vol: Current implied volatilities (single-date Series). + r: Risk-free interest rates (single-date Series). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + pv_divs: Present value of discrete dividends (required if dividend_type=DISCRETE). + q_factor: Continuous dividend yield factor (required if dividend_type=CONTINUOUS). + spot_scenarios: List of spot price multipliers. E.g., [0.9, 1.0, 1.1] tests + spot -10%, unchanged, +10%. Defaults to [1.0]. + vol_scenarios: List of volatility adjustments. E.g., [-0.05, 0.0, 0.05] tests + vol -5%, unchanged, +5%. Defaults to [0.0]. + return_pnl: If True, returns P&L relative to base_prices instead of absolute prices. + return_pnl_in_pct: If True (with return_pnl=True), returns P&L as percentage. + prettify_columns: If True, formats column/index labels for display. + + Returns: + DataFrame with volatility scenarios as rows, spot scenarios as columns, and + option prices (or P&L) as values. + + Raises: + AssertionError: If neither spot_scenarios nor vol_scenarios provided. + AssertionError: If input series contain more than one date. + AssertionError: If pv_divs not provided when dividend_type=DISCRETE. + AssertionError: If q_factor not provided when dividend_type=CONTINUOUS. + + Examples: + >>> # Internal usage - calculate scenario grid + >>> scenarios_df = _calculate_bsm_scenarios( + ... base_prices=pd.Series([10.5]), + ... s=pd.Series([150.0]), + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... vol=pd.Series([0.25]), + ... r=pd.Series([0.05]), + ... dividend_type=DivType.DISCRETE, + ... pv_divs=pd.Series([2.5]), + ... spot_scenarios=[0.9, 0.95, 1.0, 1.05, 1.1], + ... vol_scenarios=[-0.05, 0.0, 0.05], + ... prettify_columns=True + ... ) + """ + assert any([spot_scenarios, vol_scenarios]), "At least one of spot_scenarios or vol_scenarios must be provided." + assert len(vol.index) == 1, "Spot scenarios calculation only supports single-date series." + + ## Default scenarios + if spot_scenarios is None: + spot_scenarios = [1.0] + if vol_scenarios is None: + vol_scenarios = [0.0] + + if dividend_type == DivType.CONTINUOUS: + assert q_factor is not None, "For continuous dividends, q_factor must be provided." + dividends = q_factor + else: + assert pv_divs is not None, "For discrete dividends, pv_divs must be provided." + dividends = pv_divs + + ## Sync all data to same index + s, vol, r, dividends, base_prices = sync_date_index(s, vol, r, dividends, base_prices) + scenario_prices: Dict[str, pd.Series] = {} + + ## Define pricing function for reuse + def price_func( + scenario_spot: pd.Series, + scenario_vol: pd.Series, + expiration: DATE_HINT, + right: Literal["c", "p"], + strike: float, + ) -> pd.Series: + t = time_distance_helper(start=scenario_spot.index, end=[expiration] * len(scenario_spot)) + if dividend_type == DivType.CONTINUOUS: + F = vectorized_forward_continuous( + S=scenario_spot.values, + r=r.values, + q_factor=dividends.values, + T=t, + ) + else: + F = vectorized_forward_discrete( + S=scenario_spot.values, + r=r.values, + pv_divs=dividends.values, + T=t, + ) + prices = black_scholes_vectorized( + F=F, + K=[strike] * len(scenario_spot), + T=t, + r=r.values, + sigma=scenario_vol.values, + option_type=[right] * len(scenario_spot), + ) + return pd.Series(data=prices, index=scenario_spot.index, name="theoretical_price", dtype=float) + + ## Calculate prices for each scenarios + scenarios = list(product(spot_scenarios, vol_scenarios)) + for spot_mult, vol_add in scenarios: + scenario_spot = s * spot_mult + scenario_vol = vol + vol_add + + prices = price_func(scenario_spot, scenario_vol, expiration, right, strike) + prices = prices[0] + if return_pnl: + prices = prices - base_prices[0] + if return_pnl_in_pct: + prices = prices / base_prices[0] + scenario_prices.setdefault(spot_mult, []).append(prices) + + df = pd.DataFrame(scenario_prices, index=vol_scenarios) + if prettify_columns: + df.columns = [f"Spot x{col:.2f}" for col in df.columns] + df.index = [f"Vol {'+' if idx > 0 else ''}{idx:.2%}" for idx in df.index] + return df + + +def calculate_scenarios( + symbol: str, + strike: float, + expiration: DATE_HINT, + right: Literal["c", "p"], + as_of: Optional[DATE_HINT] = None, + spot_scenarios: Optional[List[float]] = None, + vol_scenarios: Optional[List[float]] = None, + *, + rt: Optional[bool] = False, + market_model: Optional[OptionPricingModel] = None, + endpoint_source: OptionSpotEndpointSource = None, + dividend_type: Optional[DivType] = None, + vol: Optional[VolatilityResult] = None, + model_price: Optional[ModelPrice] = None, + spot: Optional[SpotResult] = None, + option_spot: Optional[OptionSpotResult] = None, + r: Optional[RatesResult] = None, + d: Optional[DividendsResult] = None, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + prettify_columns: bool = False, + return_pnl: bool = False, + return_pnl_in_pct: bool = False, +) -> ScenariosResult: + """Calculate option price scenarios across spot and volatility stress levels. + + Performs scenario analysis by computing option prices across a grid of spot price + and volatility levels. Useful for risk management, stress testing, and understanding + option P&L sensitivity. Can return absolute prices or P&L relative to current market. + + Args: + symbol: Ticker symbol for the underlying asset (e.g., "AAPL", "MSFT"). + as_of: Valuation date for scenario analysis (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + expiration: Option expiration date (YYYY-MM-DD string or datetime). + right: Option type ('c' for call, 'p' for put). + spot_scenarios: List of spot price multipliers. E.g., [0.9, 0.95, 1.0, 1.05, 1.1] + tests spot at -10%, -5%, unchanged, +5%, +10%. Defaults to DEFAULT_SCENARIOS. + vol_scenarios: List of volatility adjustments (absolute). E.g., [-0.1, -0.05, 0.0, 0.05, 0.1] + tests vol at -10%, -5%, unchanged, +5%, +10%. Defaults to DEFAULT_VOL_SCENARIOS. + market_model: OptionPricingModel.BSM or BINOMIAL. Defaults to CONFIG setting. + endpoint_source: Option data source for volatility (ORATS, HIST, QUOTE). + Defaults to CONFIG setting. + dividend_type: DivType.DISCRETE or DivType.CONTINUOUS. Defaults to CONFIG setting. + vol: Optional pre-computed implied volatilities. If None, loads automatically. + model_price: Which price to use (CLOSE, OPEN, MIDPOINT). Defaults to CONFIG setting. + spot: Optional pre-computed spot prices. If None, loads automatically. + option_spot: Optional pre-computed option market prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + d: Optional pre-computed dividend data. If None, loads automatically. + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of time steps for binomial tree. Defaults to CONFIG.n_steps. + prettify_columns: If True, formats grid labels for display ("Spot x0.95", "Vol +5.00%"). + return_pnl: If True, returns P&L relative to current market price. + return_pnl_in_pct: If True (with return_pnl=True), returns P&L as percentage of market price. + rt: If True, uses real-time data where available (default False). + + Returns: + ScenariosResult containing DataFrame grid with volatility scenarios as rows, + spot scenarios as columns, and prices/P&L as values, plus model metadata. + + Examples: + >>> # Basic scenario analysis + >>> result = calculate_scenarios( + ... symbol="AAPL", + ... as_of="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... spot_scenarios=[0.9, 0.95, 1.0, 1.05, 1.1], + ... vol_scenarios=[-0.05, 0.0, 0.05], + ... prettify_columns=True + ... ) + >>> print(result.grid) + >>> + >>> # P&L analysis for risk management + >>> pnl_result = calculate_scenarios( + ... symbol="AAPL", + ... as_of="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="p", + ... spot_scenarios=[0.8, 0.9, 1.0, 1.1, 1.2], + ... vol_scenarios=[-0.1, 0.0, 0.1], + ... return_pnl=True, + ... return_pnl_in_pct=True, + ... prettify_columns=True + ... ) + >>> print(f"Worst case: {pnl_result.grid.min().min():.2%}") + >>> + >>> # Custom stress scenarios with binomial model + >>> result = calculate_scenarios( + ... symbol="AAPL", + ... as_of="2025-01-15", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="c", + ... spot_scenarios=[0.7, 0.85, 1.0, 1.15, 1.3], + ... vol_scenarios=[-0.15, -0.075, 0.0, 0.075, 0.15], + ... market_model=OptionPricingModel.BINOMIAL, + ... n_steps=200, + ... return_pnl=True + ... ) + """ + if not as_of and not rt: + raise ValueError("Either as_of date must be provided or rt=True for real-time data.") + + market_model = market_model or CONFIG.option_model + endpoint_source = endpoint_source or CONFIG.option_spot_endpoint_source + dividend_type = dividend_type or CONFIG.dividend_type + vol_model = vol or CONFIG.volatility_model + model_price = model_price or CONFIG.model_price + n_steps = n_steps or CONFIG.n_steps + spot_scenarios = spot_scenarios or DEFAULT_SCENARIOS + vol_scenarios = vol_scenarios or DEFAULT_VOL_SCENARIOS + result = ScenariosResult() + + result.dividend_type = dividend_type + result.market_model = market_model + result.model_price = model_price + result.vol_model = vol_model + result.endpoint_source = endpoint_source + result.expiration = to_datetime(expiration) + result.right = right + result.strike = strike + result.symbol = symbol + result.rt = False + result.undo_adjust = undo_adjust + result.spot_scenarios = spot_scenarios + result.vol_scenarios = vol_scenarios + result.as_of = to_datetime(as_of) if not rt else datetime.now() + result.rt = rt + + # Create load request to determine which data to load + load_request = _create_load_request( + symbol=symbol, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + market_model=market_model, + endpoint_source=endpoint_source, + model_price=model_price, + s=spot, + r=r, + d=d, + vol=vol, + option_spot=option_spot, + is_scenario_load=True, + undo_adjust=undo_adjust, + as_of=as_of, + rt=rt, + ) + + # Load required market data + packet = _load_model_data_timeseries(load_request) + + s, r, vol, base_prices, d = ( + packet.spot.timeseries if not packet.spot.is_empty() else spot.timeseries, + packet.rates.timeseries if not packet.rates.is_empty() else r.timeseries, + packet.vol.timeseries if not packet.vol.is_empty() else vol.timeseries, + packet.option_spot.price if not packet.option_spot.is_empty() else option_spot.price, + packet.dividend.timeseries if not packet.dividend.is_empty() else d.timeseries, + ) + # Use loaded data to calculate theoretical prices + if market_model == OptionPricingModel.BINOMIAL: + s, vol, r, d, base_prices = sync_date_index(s, vol, r, d, base_prices) + df = _calculate_binomial_scenarios( + base_prices=base_prices, + s=s, + strike=strike, + expiration=expiration, + right=right, + vol=vol, + r=r, + dividend_type=dividend_type, + dividends=d, + spot_scenarios=spot_scenarios, + vol_scenarios=vol_scenarios, + n_steps=n_steps, + prettify_columns=prettify_columns, + return_pnl=return_pnl, + return_pnl_in_pct=return_pnl_in_pct, + ) + result.grid = df + return result + + ## BSM model + elif market_model == OptionPricingModel.BSM: + s, vol, r, d, base_prices = sync_date_index(s, vol, r, d, base_prices) + if dividend_type == DivType.DISCRETE: + pv_divs = vectorized_discrete_pv( + schedules=d.values, + _valuation_dates=s.index, + _end_dates=[to_datetime(expiration)] * len(s), + r=r.values, + ) + pv_divs = pd.Series(data=pv_divs, index=s.index, name="pv_dividends", dtype=float) + q_factor = None + else: + pv_divs = None + q_factor = get_vectorized_continuous_dividends( + div_rates=d.values, + _valuation_dates=s.index, + _end_dates=[to_datetime(expiration)] * len(s), + ) + q_factor = pd.Series(data=q_factor, index=s.index, name="q_factor", dtype=float) + df = _calculate_bsm_scenarios( + base_prices=base_prices, + s=s, + strike=strike, + expiration=expiration, + right=right, + vol=vol, + r=r, + dividend_type=dividend_type, + pv_divs=pv_divs, + q_factor=q_factor, + spot_scenarios=spot_scenarios, + vol_scenarios=vol_scenarios, + prettify_columns=prettify_columns, + return_pnl=return_pnl, + return_pnl_in_pct=return_pnl_in_pct, + ) + result.grid = df + return result diff --git a/trade/datamanager/timeseries.py b/trade/datamanager/timeseries.py new file mode 100644 index 0000000..a92c63c --- /dev/null +++ b/trade/datamanager/timeseries.py @@ -0,0 +1,318 @@ +"""Unified timeseries interface for all DataManager classes. + +This module provides TimeseriesDataManager and TimeseriesAdapter classes that create +a consistent, simplified API across all specialized data managers (spot, vol, greeks, +etc.). Each manager's specific method names are mapped to standardized names while +preserving original docstrings and type signatures. + +Key Features: + - Standardized API: rt(), get_at_time(), get_timeseries() across all managers + - Preserves original docstrings and signatures for IDE support + - Property-based access to underlying managers via adapters + - Pass-through to underlying manager attributes when needed + - Single entry point for all market data retrieval + +Typical Usage: + >>> from trade.datamanager.timeseries import TimeseriesDataManager + >>> + >>> # Initialize for a symbol + >>> ts = TimeseriesDataManager("AAPL") + >>> + >>> # Spot data with consistent interface + >>> spot_rt = ts.spot.rt() + >>> spot_series = ts.spot.get_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31" + ... ) + >>> + >>> # Options data (pass strike/expiration/right explicitly) + >>> vol = ts.vol.get_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call" + ... ) + >>> + >>> # Greeks with same consistent interface + >>> greeks = ts.greeks.rt( + ... strike=150.0, + ... expiration="2025-06-20", + ... right="call" + ... ) + >>> + >>> # Access underlying manager if needed + >>> underlying_vol_mgr = ts.vol._manager + +Architecture: + TimeseriesDataManager acts as a facade, exposing properties (spot, vol, greeks, etc.) + that return TimeseriesAdapter instances. Each adapter wraps the underlying DataManager + and provides the standardized method names that delegate to the actual methods. +""" + +from typing import Any, Optional +import inspect +from trade.helpers.Logging import setup_logger +from .spot import SpotDataManager +from .vol import VolDataManager +from .dividend import DividendDataManager +from .forward import ForwardDataManager +from .option_spot import OptionSpotDataManager +from .greeks import GreekDataManager +from .rates import RatesDataManager +from trade.datamanager.utils.logging import get_logging_level, register_to_factor_list + +logger = setup_logger("trade.datamanager.timeseries", stream_log_level=get_logging_level()) +register_to_factor_list("trade.datamanager.timeseries") + + +class TimeseriesAdapter: + """Adapter that provides a consistent interface for any DataManager. + + Maps standardized method names (rt, get_at_time, get_timeseries) to + the actual underlying DataManager methods while preserving original + docstrings, signatures, and type hints. + """ + + def __init__( + self, + manager: Any, + rt_method: Optional[str] = "rt", + get_at_time_method: Optional[str] = None, + get_timeseries_method: Optional[str] = None, + ): + """Initialize adapter with method name mappings. + + Args: + manager: The underlying DataManager instance + rt_method: Name of the real-time method (default: "rt") + get_at_time_method: Name of the get-at-time method (e.g., "get_at_time", "get_at_time_implied_volatility") + get_timeseries_method: Name of the timeseries method (e.g., "get_spot_timeseries", "get_implied_volatility_timeseries") + """ + self._manager = manager + self._rt_method = rt_method + self._get_at_time_method = get_at_time_method + self._get_timeseries_method = get_timeseries_method + + # Create wrapper methods with copied metadata + self._create_wrapper_method("rt", rt_method) + self._create_wrapper_method("get_at_time", get_at_time_method) + self._create_wrapper_method("get_timeseries", get_timeseries_method) + + def _create_wrapper_method(self, wrapper_name: str, underlying_method_name: Optional[str]): + """Create a wrapper method that copies docstring and signature from underlying method. + + Args: + wrapper_name: Name of the wrapper method on this adapter + underlying_method_name: Name of the actual method on the underlying manager + """ + if not underlying_method_name or not hasattr(self._manager, underlying_method_name): + return + + underlying_method = getattr(self._manager, underlying_method_name) + + # Create wrapper function that calls the underlying method + def wrapper(*args, **kwargs): + return underlying_method(*args, **kwargs) + + # Copy metadata for proper introspection + wrapper.__name__ = wrapper_name + wrapper.__doc__ = underlying_method.__doc__ + wrapper.__wrapped__ = underlying_method # Allows ? to find original source + + # Copy module and qualname for correct file location display + if hasattr(underlying_method, "__module__"): + wrapper.__module__ = underlying_method.__module__ + if hasattr(underlying_method, "__qualname__"): + # Keep the wrapper name but use the module path + wrapper.__qualname__ = f"{underlying_method.__qualname__.rsplit('.', 1)[0]}.{wrapper_name}" + + # Copy signature + try: + wrapper.__signature__ = inspect.signature(underlying_method) + except (ValueError, TypeError): + pass + + # Copy annotations if available + if hasattr(underlying_method, "__annotations__"): + wrapper.__annotations__ = underlying_method.__annotations__.copy() + + # Set as instance attribute (overrides class method) + setattr(self, wrapper_name, wrapper) + + def rt(self, *args, **kwargs): + """Call the underlying manager's real-time method.""" + if self._rt_method and hasattr(self._manager, self._rt_method): + method = getattr(self._manager, self._rt_method) + return method(*args, **kwargs) + raise NotImplementedError(f"{self._manager.__class__.__name__} does not support rt()") + + def get_at_time(self, *args, **kwargs): + """Call the underlying manager's get-at-time method.""" + if self._get_at_time_method and hasattr(self._manager, self._get_at_time_method): + method = getattr(self._manager, self._get_at_time_method) + return method(*args, **kwargs) + raise NotImplementedError(f"{self._manager.__class__.__name__} does not support get_at_time()") + + def get_timeseries(self, *args, **kwargs): + """Call the underlying manager's timeseries method.""" + if self._get_timeseries_method and hasattr(self._manager, self._get_timeseries_method): + method = getattr(self._manager, self._get_timeseries_method) + return method(*args, **kwargs) + raise NotImplementedError(f"{self._manager.__class__.__name__} does not support get_timeseries()") + + def __getattr__(self, name: str): + """Pass through any other attribute access to the underlying manager.""" + return getattr(self._manager, name) + + +class TimeseriesDataManager: + """Unified interface for all data managers with consistent method naming. + + Each data manager is wrapped with a TimeseriesAdapter that maps standardized + method names (rt, get_at_time, get_timeseries) to the actual underlying methods. + + Examples: + >>> # Basic spot data access + >>> ts = TimeseriesDataManager("AAPL") + >>> spot_result = ts.spot.rt() + >>> spot_series = ts.spot.get_timeseries(start_date="2025-01-01", end_date="2025-01-31") + + >>> # Options data - pass parameters explicitly + >>> ts = TimeseriesDataManager("AAPL") + >>> vol_result = ts.vol.rt(strike=150.0, expiration="2025-06-20", right="call") + >>> greeks = ts.greeks.get_timeseries( + ... start_date="2025-01-01", end_date="2025-01-31", + ... strike=150.0, expiration="2025-06-20", right="call" + ... ) + """ + + def __init__(self, symbol: str): + """Initialize unified timeseries data manager. + + Args: + symbol: Ticker symbol (e.g., "AAPL") + """ + self.symbol = symbol + + # Initialize underlying managers + self._spot_manager = SpotDataManager(symbol=symbol) + self._vol_manager = VolDataManager(symbol=symbol) + self._dividend_manager = DividendDataManager(symbol=symbol) + self._forward_manager = ForwardDataManager(symbol=symbol) + self._option_spot_manager = OptionSpotDataManager(symbol=symbol) + self._greeks_manager = GreekDataManager(symbol=symbol) + self._rates_manager = RatesDataManager() + + @property + def spot(self) -> TimeseriesAdapter: + """Access spot price data with standardized interface. + + Methods: + - rt(): Get real-time spot price + - get_at_time(date): Get spot price at specific date + - get_timeseries(start_date, end_date, undo_adjust=True): Get spot price series + """ + return TimeseriesAdapter( + manager=self._spot_manager, + rt_method="rt", + get_at_time_method="get_at_time", + get_timeseries_method="get_spot_timeseries", + ) + + @property + def vol(self) -> TimeseriesAdapter: + """Access implied volatility data with standardized interface. + + Methods: + - rt(strike, expiration, right, ...): Get real-time implied volatility + - get_at_time(date, strike, expiration, right, ...): Get implied vol at specific date + - get_timeseries(start_date, end_date, strike, expiration, right, ...): Get implied vol series + """ + return TimeseriesAdapter( + manager=self._vol_manager, + rt_method="rt", + get_at_time_method="get_at_time_implied_volatility", + get_timeseries_method="get_implied_volatility_timeseries", + ) + + @property + def greeks(self) -> TimeseriesAdapter: + """Access option greeks data with standardized interface. + + Requires: strike, expiration, right parameters set in constructor + + Methods: + - rt(strike, expiration, right, ...): Get real-time option greeks + - get_at_time(date, strike, expiration, right, ...): Get greeks at specific date + - get_timeseries(start_date, end_date, strike, expiration, right, ...): Get greeks series + """ + return TimeseriesAdapter( + manager=self._greeks_manager, + rt_method="rt", + get_at_time_method="get_at_time_greeks", + get_timeseries_method="get_greeks_timeseries", + ) + + @property + def forward(self) -> TimeseriesAdapter: + """Access forward price data with standardized interface. + + Methods: + - rt(maturity_date): Get real-time forward price + - get_timeseries(start_date, end_date, maturity_date): Get forward price series + """ + return TimeseriesAdapter( + manager=self._forward_manager, + rt_method="rt", + get_at_time_method="get_forward", # Forward doesn't have get_at_time + get_timeseries_method="get_forward_timeseries", + ) + + @property + def dividend(self) -> TimeseriesAdapter: + """Access dividend data with standardized interface. + + Methods: + - rt(maturity_date): Get real-time dividend schedule + - get_timeseries(start_date, end_date, maturity_date): Get dividend series + """ + return TimeseriesAdapter( + manager=self._dividend_manager, + rt_method="rt", + get_at_time_method="get_schedule", # Dividend doesn't have get_at_time + get_timeseries_method="get_schedule_timeseries", + ) + + @property + def rates(self) -> TimeseriesAdapter: + """Access risk-free rate data with standardized interface. + + Methods: + - rt(): Get real-time risk-free rate + - get_timeseries(start_date, end_date): Get rate series + """ + return TimeseriesAdapter( + manager=self._rates_manager, + rt_method="rt", + get_at_time_method="get_rate", # Rates doesn't have get_at_time + get_timeseries_method="get_risk_free_rate_timeseries", + ) + + @property + def option_spot(self) -> TimeseriesAdapter: + """Access option market price data with standardized interface. + + Requires: strike, expiration, right parameters set in constructor + + Methods: + - rt(strike, expiration, right, ...): Get real-time option market price + - get_at_time(date, strike, expiration, right, ...): Get option price at specific date + - get_timeseries(start_date, end_date, strike, expiration, right, ...): Get option price series + """ + return TimeseriesAdapter( + manager=self._option_spot_manager, + rt_method="rt", + get_at_time_method="get_option_spot_at_time", + get_timeseries_method="get_option_spot_timeseries", + ) diff --git a/trade/datamanager/utils/__init__.py b/trade/datamanager/utils/__init__.py new file mode 100644 index 0000000..968c5f2 --- /dev/null +++ b/trade/datamanager/utils/__init__.py @@ -0,0 +1,14 @@ +from bisect import bisect_left, bisect_right +from datetime import date +from typing import List +from trade.optionlib.assets.dividend import ScheduleEntry + +def slice_schedule(full_schedule: List[ScheduleEntry], val_date: date, mat_date: date) -> List[ScheduleEntry]: + """ + Return entries in full_schedule with entry.date in [val_date, mat_date]. + Assumes full_schedule is sorted by entry.date ascending and each entry has .date (datetime.date). + """ + dates = [e.date for e in full_schedule] + i0 = bisect_left(dates, val_date) + i1 = bisect_right(dates, mat_date) + return full_schedule[i0:i1] \ No newline at end of file diff --git a/trade/datamanager/utils/cache.py b/trade/datamanager/utils/cache.py new file mode 100644 index 0000000..2bdadf7 --- /dev/null +++ b/trade/datamanager/utils/cache.py @@ -0,0 +1,162 @@ +import pandas as pd +from datetime import date +from typing import Any, List, Optional, Union, Tuple +from trade.helpers.Logging import setup_logger +from trade.helpers.helper import CustomCache, get_missing_dates +from .date import _should_save_today, DATE_HINT +from ..base import BaseDataManager +from .data_structure import _data_structure_sanitize +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + + +def _data_structure_cache_it( + self: BaseDataManager, + key: str, + value: Union[pd.Series, pd.DataFrame], + *, + expire: Optional[int] = None, +): + """Merges and caches rate timeseries, excluding today's partial data.""" + value = value.copy() + if not isinstance(value, (pd.Series, pd.DataFrame)): + raise TypeError(f"Expected pd.Series or pd.DataFrame for caching, got {type(value)}") + + if not isinstance(value.index, pd.DatetimeIndex): + raise TypeError("Expected DatetimeIndex for caching timeseries data.") + + if not isinstance(self, BaseDataManager): + raise TypeError(f"{self.__class__.__name__} must be a subclass of BaseDataManager.") + + existing: Optional[Union[pd.Series, pd.DataFrame]] = self.get(key, default=None) + _cache_it_timeseries_data_structure( + existing=existing, + key=key, + value=value, + expire=expire, + cache=self, + ) + +def _cache_it_timeseries_data_structure( + existing: Union[pd.Series, pd.DataFrame], + key: str, + value: Union[pd.Series, pd.DataFrame], + expire: Optional[int] = None, + cache: CustomCache = None, + skip_today_check: bool = False, +): + """Caches a timeseries data structure, merging with existing data and handling today's data.""" + assert isinstance(value, (pd.Series, pd.DataFrame)), f"Expected pd.Series or pd.DataFrame for caching, got {type(value)}" + assert isinstance(existing, (pd.Series, pd.DataFrame, type(None))), f"Expected pd.Series, pd.DataFrame, or None for existing data, got {type(existing)}" + + ## Since it is a timeseries, we will append to existing if exists + if existing is not None: + # Merge existing and new values. We're expecting pd.Series + merged = pd.concat([existing, value]) + value = merged[~merged.index.duplicated(keep="last")] + + if value.empty: + logger.info(f"Not caching empty timeseries for key: {key}") + return + + if not _should_save_today(max_date=value.index.max().date()) and not skip_today_check: + logger.info(f"Cutting off today's data for key: {key} to avoid saving partial day data.") + value = value[value.index < pd.to_datetime(date.today())] + + ## Do not cache rules: + cache_data = True + + ## 1) If after removing today's data, there is no data left + if value.empty: + cache_data = False + logger.info(f"No data left to cache for key: {key} after removing today's data.") + + ## 2) If all data points are NaN + if value.isna().all().all(): + cache_data = False + logger.info(f"All data points are NaN for key: {key}. Not caching.") + + + if not cache_data: + return + + + value.sort_index(inplace=True) + + cache.set(key, value, expire=expire) + + +def _simple_list_cache_it(self: BaseDataManager, key: str, value: List[Any], *, expire: Optional[int] = None): + """Cache a list of simple values. Will append and keep unique. Also sort""" + + if not isinstance(value, list): + raise TypeError(f"Expected list. Recieved {type(value)}") + + existing: List = self.get(key, default=[]) + existing.extend(value) + existing = sorted(list(set(existing))) + self.set(key, existing, expire=expire) + +def _check_cache_for_timeseries_data_structure( + self: BaseDataManager, + key: str, + start_dt: DATE_HINT, + end_dt: DATE_HINT, +) -> Tuple[Optional[Union[pd.Series, pd.DataFrame]], bool, DATE_HINT, DATE_HINT]: + """ + Checks cache for existing timeseries data structure and identifies missing dates. + + Return args order: + - cached_data: The cached pd.Series or pd.DataFrame if fully present, else None + - is_partial: True if some dates are missing, False if fully present + - missing_start_date: The earliest missing date if partially present, else start_dt + - missing_end_date: The latest missing date if partially present, else end_dt + """ + + cached_data = self.get(key, default=None) + if not isinstance(self, BaseDataManager): + raise TypeError(f"{self.__class__.__name__} must be a subclass of BaseDataManager.") + + if not isinstance(cached_data, (pd.Series, pd.DataFrame, type(None))): + return None, False, start_dt, end_dt + + if cached_data is None: + return None, False, start_dt, end_dt + + return _data_structure_cache_check_missing( + cached_data=cached_data, + key=key, + start_dt=start_dt, + end_dt=end_dt, + ) + +def _data_structure_cache_check_missing( + cached_data: Union[pd.Series, pd.DataFrame], + key: str, + start_dt: DATE_HINT, + end_dt: DATE_HINT, +) -> Tuple[Union[pd.Series, pd.DataFrame], bool, DATE_HINT, DATE_HINT]: + """ + Checks cached timeseries data structure for missing dates within a specified range. + Return args order: + - cached_data: The cached pd.Series or pd.DataFrame, sanitized to the requested date range + - is_partial: True if some dates are missing, False if fully present + - missing_start_date: The earliest missing date if partially present, else start_dt + - missing_end_date: The latest missing date if partially present, else end_dt + """ + + missing = get_missing_dates(cached_data, _start=start_dt, _end=end_dt) + if not missing: + logger.info(f"Cache hit for timeseries data structure key: {key}") + cached_data = _data_structure_sanitize( + cached_data, + start=start_dt, + end=end_dt, + source_name=f"cached timeseries for key {key}", + ) + return cached_data, False, start_dt, end_dt + logger.info( + f"Cache partially covers requested date range for timeseries data structure. " + f"Key: {key}. Fetching missing dates: {missing}" + ) + return cached_data, True, min(missing), max(missing) diff --git a/trade/datamanager/utils/data_structure.py b/trade/datamanager/utils/data_structure.py new file mode 100644 index 0000000..3c56803 --- /dev/null +++ b/trade/datamanager/utils/data_structure.py @@ -0,0 +1,64 @@ +from datetime import datetime +import numpy as np +from typing import Union +import pandas as pd +from trade.datamanager.exceptions import EmptyDataException +from trade.helpers.helper import to_datetime +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME +from trade import HOLIDAY_SET + +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + + +def _data_structure_sanitize( + df: Union[pd.Series, pd.DataFrame], + start: Union[datetime, str], + end: Union[datetime, str], + source_name: str = "", +) -> Union[pd.Series, pd.DataFrame]: + """Sanitizes the data structure by removing duplicates and sorting the index.""" + logger.info(f"Sanitizing data from {start} to {end}...") + if not isinstance(df, (pd.Series, pd.DataFrame)): + raise TypeError(f"Expected pd.Series or pd.DataFrame for sanitization, got {type(df)}") + + # Ensure DatetimeIndex. If not, attempt conversion + if not isinstance(df.index, pd.DatetimeIndex): + try: + df.index = to_datetime(df.index, format="%Y-%m-%d") + except Exception as e: + raise TypeError("Expected DatetimeIndex for sanitization of timeseries data.") from e + + # Remove duplicates, keeping the last occurrence + df = df[~df.index.duplicated(keep="last")] + + # Sort the index + df = df.sort_index() + + # if dataframe, lower case columns + if isinstance(df, pd.DataFrame): + df.columns = df.columns.str.lower() + + # Filter by start and end dates + df = df[(df.index.date >= pd.to_datetime(start).date()) & (df.index.date <= pd.to_datetime(end).date())] + + if df.empty: + raise EmptyDataException(f"No data available after sanitization between {start} and {end}. Source: {source_name}") + + # Re-sort after filtering + df = df.sort_index() + + # Index name=datetime + df.index.name = "datetime" + + # Resample to business day frequency if not already and fill missing dates with NaN + all_bus_days = pd.date_range(start=df.index.min(), end=df.index.max(), freq="B") + all_bus_days = [d for d in all_bus_days if d.strftime("%Y-%m-%d") not in HOLIDAY_SET] + df = df.reindex(all_bus_days, fill_value=np.nan) + + # Filter out holidays + df = df[~df.index.strftime("%Y-%m-%d").isin(HOLIDAY_SET)] + + + + return df diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py new file mode 100644 index 0000000..403d908 --- /dev/null +++ b/trade/datamanager/utils/date.py @@ -0,0 +1,231 @@ +import pandas as pd +from dataclasses import dataclass +from datetime import datetime, date +from pandas.tseries.offsets import BDay +from trade.helpers.helper import to_datetime, is_busday, is_USholiday +from trade.helpers.helper import ny_now +from trade.optionlib.assets.dividend import SECONDS_IN_DAY, SECONDS_IN_YEAR # noqa +from trade.datamanager.vars import TODAY_RELOAD_CUTOFF, MIN_TIME_BEFORE_REAL_TIME +from trade.helpers.helper_types import DATE_HINT +from trade.helpers.helper import time_distance_helper # noqa +from trade.helpers.helper import CustomCache, generate_option_tick_new +from trade.datamanager._enums import OptionSpotEndpointSource +from trade.helpers.helper import is_market_hours_today +from trade.helpers.helper_types import is_iterable # noqa +from trade.helpers.Logging import setup_logger +from trade.optionlib.utils.format import assert_equal_length # noqa +from dbase.DataAPI.ThetaData import list_dates +from pathlib import Path +import os +from typing import Tuple, List, Optional, Union +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + +PATH = Path(os.environ["GEN_CACHE_PATH"]) / "dm_gen_cache" + +## This cache will be used to save the min trading date for each option tick +## This is to avoid calling API all the time + + + +LIST_DATE_CACHE = CustomCache( + location=PATH.as_posix(), + fname="list_date_cache", + clear_on_exit=False, + expire_days=365, +) + +def sync_date_index(*args) -> List[Union[pd.Series, pd.DataFrame]]: + """Synchronizes the date indices of multiple time series.""" + for i, ts in enumerate(args): + if ts is None: + raise ValueError("All time series must be provided and not None. Found None at position {}".format(i)) + if not isinstance(ts, (pd.Series, pd.DataFrame)): + raise TypeError( + "All inputs must be pandas Series or DataFrame. Found {} at position {}".format(type(ts), i) + ) + date_indices = [set(ts.index) for ts in args if ts is not None] + common_dates = list(set.intersection(*date_indices)) + synced_series = [ts.loc[common_dates] if ts is not None else None for ts in args] + synced_series = [ts.sort_index() if ts is not None else None for ts in synced_series] + return synced_series + + + + +def _sync_date( + symbol: str, + start_date: DATE_HINT, + end_date: DATE_HINT, + strike: Optional[float] = None, + expiration: Optional[Union[datetime, str]] = None, + right: Optional[str] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = OptionSpotEndpointSource.EOD, +) -> Tuple[datetime, datetime]: + """Synchronizes requested dates with available data range from Thetadata. + + Queries Thetadata for available dates for the specified option contract and + adjusts the requested date range to fit within available data bounds. + + Args: + start_date: Requested start date. + end_date: Requested end date. + strike: Option strike price. + expiration: Option expiration date. + right: Option type ("C" for call, "P" for put). + endpoint_source: Source of option spot data. + + Returns: + Tuple of (adjusted_start_date, adjusted_end_date) constrained to + available data range. + + Examples: + >>> opt_mgr = OptionSpotDataManager("AAPL") + >>> start, end = opt_mgr._sync_date( + ... start_date="2025-01-01", + ... end_date="2025-12-31", + ... strike=150.0, + ... expiration="2025-06-20", + ... right="C" + ... ) + + Notes: + - Constrains start_date to max(requested_start, min_available_date) + - Constrains end_date to min(requested_end, max_available_date) + - Prevents requesting dates outside available data range + """ + + ## Process Note: + ## list of dates is only important for min date + ## Once we have min date, all dates after that are available until expiration or today + ## For end date, we just need to compare requested end date with expiration/today. + ## There is added logic for EOD source since data is only available after market close + opttick = generate_option_tick_new( + symbol=symbol, + exp=expiration, + right=right, + strike=strike, + ) + + def _get_max_date(requested_end: datetime) -> datetime: + """ + Determines the maximum allowable end date based on requested end date, + option expiration, and data source constraints. + + Note: We don't really need list of dates. min_date is < requested_date, all dates in between are available + + Args: + requested_end: The originally requested end date. + """ + + if to_datetime(requested_end) <= to_datetime(expiration): + ## EOD report is produced after 6pm, + ## so max date is prev bus day as long as it is trading hours + if endpoint_source == OptionSpotEndpointSource.EOD: + max_allowed = prev_busday if is_market_hrs else today + else: + max_allowed = today + + ## Get max date within allowed range + max_date = to_datetime(min(max_allowed.date(), to_datetime(requested_end).date())) + + ## Else, max date is expiration + else: + max_date = to_datetime(expiration) + + return max_date + + is_market_hrs = is_market_hours_today() + today = ny_now() + prev_busday = (today - BDay(1)).to_pydatetime() + start_date = to_datetime(start_date) + end_date = to_datetime(end_date) + + if opttick in LIST_DATE_CACHE.keys(): + logger.info(f"Using cached date range for {start_date} - {end_date} and option tick {opttick}") + cached_dates = LIST_DATE_CACHE.get(key=opttick) + min_date = cached_dates["min_date"] + max_date = _get_max_date(end_date) + + start_date = max(min_date, start_date) + end_date = min(max_date, end_date) + return min(start_date, end_date), max(start_date, end_date) + + logger.info(f"Fetching date range from Thetadata for {opttick}") + dates = list_dates( + symbol=symbol, + exp=expiration, + right=right, + strike=strike, + ) + + if not dates: + raise ValueError(f"No trading dates found for {opttick}") + + dates = to_datetime(dates) + + ## Adjust start date to min + min_date = min(dates) + start_date = max(min_date, start_date) + end_date = _get_max_date(end_date) + + LIST_DATE_CACHE.set(key=opttick, value={"min_date": min_date}, expire=None) + + return min(start_date, end_date), max(start_date, end_date) + + +@dataclass(slots=True) +class DateRangePacket: + """ + Simple container for start/end date ranges with both datetime and string formats. + """ + + start_date: DATE_HINT + end_date: DATE_HINT + start_str: Optional[str] = None + end_str: Optional[str] = None + maturity_date: Optional[DATE_HINT] = None + maturity_str: Optional[str] = None + + def __post_init__(self): + self.start_date = to_datetime(self.start_date) + self.end_date = to_datetime(self.end_date) + if self.maturity_date is not None: + self.maturity_date = to_datetime(self.maturity_date) + + self.start_str = self.start_str or self.start_date.strftime("%Y-%m-%d") + self.end_str = self.end_str or self.end_date.strftime("%Y-%m-%d") + if self.maturity_date is not None: + self.maturity_str = self.maturity_str or self.maturity_date.strftime("%Y-%m-%d") + else: + self.maturity_str = None + + +def _should_save_today(max_date: date) -> bool: + """ + Determines if data should be saved today based on the max_date and current time in New York. + """ + today = date.today() + current_time = ny_now().time() + return max_date >= today and current_time >= TODAY_RELOAD_CUTOFF + + +def is_available_on_date(date: date) -> bool: + """ + Returns True if the given date is a business day and not a US holiday, False otherwise. + For when date == today, it checks current time to see if market is open. + """ + date = to_datetime(date) + is_today = date.date() == ny_now().date() + is_trading_day = is_busday(date) and not is_USholiday(date) + + ## If both today and trading day, check time + if is_today and is_trading_day: + current_time = ny_now().time() + + ## If before min time, return False + if current_time < MIN_TIME_BEFORE_REAL_TIME: + return False + + ## Else just return trading day status + return is_trading_day diff --git a/trade/datamanager/utils/enums_utils.py b/trade/datamanager/utils/enums_utils.py new file mode 100644 index 0000000..d09a051 --- /dev/null +++ b/trade/datamanager/utils/enums_utils.py @@ -0,0 +1,76 @@ +from datetime import date, datetime, time +from enum import Enum +from typing import Dict, Optional, Any, Union +from .._enums import Interval, ArtifactType, SeriesId + +DATE_HINT = Union[datetime, str] +def _norm_str(x: str) -> str: + return x.strip().upper() + + +def _safe_part(x: Optional[str]) -> str: + return x if x not in (None, "", "None") else "-" + +def _format_value(v: Any) -> str: + """ + Keep it simple + deterministic. + """ + if v is None: + return "-" + if isinstance(v, Enum): + return str(v.value) + if isinstance(v, str): + return _norm_str(v) + if isinstance(v, bool): + return "1" if v else "0" + if isinstance(v, (int,)): + return str(v) + if isinstance(v, float): + # avoid 0.30000000000004 style keys + return f"{v:.12g}" + if isinstance(v, datetime): + # stable, compact. (no tz handling by design here) + return v.strftime("%Y%m%dT%H%M%S") + if isinstance(v, date): + return v.strftime("%Y%m%d") + + if isinstance(v, time): + return v.strftime("%H%M%S") + return str(v) + + +def construct_cache_key( + symbol: str, + interval: Optional[Interval], + artifact_type: ArtifactType, + series_id: SeriesId, + **extra_parts: Any, +) -> str: + """Constructs deterministic cache key from symbol, interval, artifact type, series ID, and extra parts.""" + + if series_id in (SeriesId.AT_TIME, SeriesId.SNAPSHOT): + assert "time" in extra_parts, "time must be provided for at_time or snapshot series_id" + assert "date" in extra_parts, "date must be provided for at_time or snapshot series_id" + assert isinstance(extra_parts["time"], time), "time must be a time object" + assert isinstance(extra_parts["date"], date), "date must be a date object" + + parts = [ + f"symbol:{_norm_str(symbol)}", + f"interval:{_format_value(interval)}", + f"artifact_type:{artifact_type.value}", + f"series_id:{series_id.value}", + ] + + for k in sorted(extra_parts.keys()): + parts.append(f"{k}:{_format_value(extra_parts[k])}") + + return "|".join(parts) + +def _parse_cache_key(key: str) -> Dict[str, str]: + """Parses a pipe-delimited cache key into a dictionary of key-value pairs.""" + parts = key.split("|") + result = {} + for part in parts: + k, v = part.split(":", 1) + result[k] = v + return result \ No newline at end of file diff --git a/trade/datamanager/utils/greeks_helpers.py b/trade/datamanager/utils/greeks_helpers.py new file mode 100644 index 0000000..46eeea7 --- /dev/null +++ b/trade/datamanager/utils/greeks_helpers.py @@ -0,0 +1,66 @@ +from typing import List, Optional, Union, Iterable +import numpy as np +from trade.helpers.helper_types import DATE_HINT +from trade.datamanager._enums import (GreekType, ModelPrice, OptionPricingModel, OptionSpotEndpointSource, VolatilityModel) +from trade.datamanager.result import GreekResultSet +from trade.optionlib.config.types import DivType +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + +def _get_prefilled_greek_result_set( + key: str, + symbol: str, + strike: float, + expiration: DATE_HINT, + right: str, + endpoint_source: OptionSpotEndpointSource, + market_model: OptionPricingModel, + vol_model: VolatilityModel, + dividend_type: DivType, + undo_adjust: bool = True, + model_price: Optional[ModelPrice] = None, +) -> GreekResultSet: + """Utility to create prefilled GreekResultSet with metadata.""" + result = GreekResultSet( + key=key, + symbol=symbol, + strike=strike, + expiration=expiration, + right=right, + endpoint_source=endpoint_source, + market_model=market_model, + vol_model=vol_model, + dividend_type=dividend_type, + undo_adjust=undo_adjust, + model_price=model_price + ) + return result + + +def _prepare_greeks_to_compute( + greeks_to_compute: Optional[Union[GreekType, Iterable[GreekType]]] = None, +) -> List[GreekType]: + + ## If None, set to all greeks + if greeks_to_compute is None: + greeks_to_compute = GreekType.GREEKS + + ## Expand GREEKS to all greek types + if greeks_to_compute == GreekType.GREEKS: + greeks_to_compute = list(set(GreekType) - {GreekType.GREEKS, GreekType.VANNA}) + + ## Validate greek_to_compute is list/tuple/set of GreekType + if not isinstance(greeks_to_compute, (list, np.ndarray, set, tuple)): + greeks_to_compute = [greeks_to_compute] + + ## Validate all elements are GreekType + if not all(isinstance(greek, GreekType) for greek in greeks_to_compute): + raise ValueError(f"greeks_to_compute must be a GreekType or list of GreekType. Found: {greeks_to_compute}") + + ## Validate no duplicates + greeks_to_compute = list(set(greeks_to_compute)) + + return list(greeks_to_compute) + + \ No newline at end of file diff --git a/trade/datamanager/utils/logging.py b/trade/datamanager/utils/logging.py new file mode 100644 index 0000000..0ee405a --- /dev/null +++ b/trade/datamanager/utils/logging.py @@ -0,0 +1,72 @@ +import logging + +from git import List +from trade.helpers.Logging import setup_logger, find_loggers_by_pattern, change_logger_stream_level +LOGGING_LEVEL = "DEBUG" +logger = setup_logger("trade.datamanager.utils", stream_log_level=LOGGING_LEVEL) + +FACTOR_DMS = { + "trade.datamanager.spot", + "trade.datamanager.rates", + "trade.datamanager.dividends", + "trade.datamanager.forward", + "trade.datamanager.vol", + "trade.datamanager.option_spot", + "trade.datamanager.greeks", + "trade.datamanager.base" +} + +VARS = [ + "trade.datamanager.vars", +] + +UTILS_LOGGER_NAME = "trade.datamanager.utils" + +def set_logging_level(level: str): + global LOGGING_LEVEL + LOGGING_LEVEL = level + +def get_logging_level() -> str: + return LOGGING_LEVEL + +def get_datamanager_loggers() -> List[logging.Logger]: + """Retrieve all loggers under 'trade.datamanager'""" + return find_loggers_by_pattern("trade.datamanager") + +def change_logging_in_all_datamanager_loggers(level: str = None): + """Change logging level for all loggers under 'trade.datamanager'.""" + if level is None: + level = LOGGING_LEVEL + loggers = find_loggers_by_pattern("trade.datamanager") + for logger in loggers: + change_logger_stream_level(logger, getattr(logging, level.upper(), logging.INFO)) + +def change_datamanager_utils_logging_level(level: str = None): + """Change logging level for 'trade.datamanager.utils' logger.""" + if level is None: + level = LOGGING_LEVEL + logger = logging.getLogger("trade.datamanager.utils") + change_logger_stream_level(logger, getattr(logging, level.upper(), logging.INFO)) + +def change_datamanager_factor_loggers_level(level: str = None): + """Change logging level for all factor loggers under 'trade.datamanager'.""" + if level is None: + level = LOGGING_LEVEL + for factor in FACTOR_DMS: + loggers = find_loggers_by_pattern(factor) + for logger in loggers: + change_logger_stream_level(logger, getattr(logging, level.upper(), logging.INFO)) + + +def change_all_optionlib_loggers_level(level: str = None): + """Change logging level for all loggers under 'trade.optionlib'""" + if level is None: + level = LOGGING_LEVEL + loggers = find_loggers_by_pattern("trade.optionlib") + for logger in loggers: + change_logger_stream_level(logger, getattr(logging, level.upper(), logging.INFO)) + + +def register_to_factor_list(name:str): + FACTOR_DMS.add(name) + diff --git a/trade/datamanager/utils/model.py b/trade/datamanager/utils/model.py new file mode 100644 index 0000000..9e76952 --- /dev/null +++ b/trade/datamanager/utils/model.py @@ -0,0 +1,674 @@ +import time +from trade.helpers.Logging import setup_logger +from trade.datamanager.result import ( + DividendsResult, + ModelResultPack, + SpotResult, + ForwardResult, + RatesResult, + VolatilityResult, + OptionSpotResult, + GreekResultSet, +) +from trade.datamanager._enums import ModelPrice, SeriesId +from trade.datamanager.requests import LoadRequest +from trade.datamanager.utils.date import DateRangePacket +from trade.datamanager.config import OptionDataConfig +from typing import Optional, Union +import pandas as pd +from trade.datamanager._enums import OptionSpotEndpointSource, VolatilityModel, OptionPricingModel +from trade.optionlib.config.types import DivType +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME +from trade.datamanager.vars import add_to_log_bucket + +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + + +def log_model_load_info( + log_info: dict, + is_rt: bool, + is_timeseries: bool, + symbol: str, + expiration: str, + strike: float, + right: str, + dividend_type: str, + market_model: str, +) -> None: + """Logs model load information in a structured format.""" + log_info["symbol"] = symbol + log_info["expiration"] = expiration + log_info["strike"] = strike + log_info["right"] = right + log_info["dividend_type"] = dividend_type + log_info["market_model"] = market_model + log_info["is_rt"] = is_rt + log_info["is_timeseries"] = is_timeseries + log_info["date"] = pd.Timestamp.now().date().strftime("%Y-%m-%d") + add_to_log_bucket(log_info) + + +def _adjust_div_yield_for_spot_shock( + shock: float, + div: float, +) -> float: + """Adjust dividend yield based on spot price shock for continuous dividends.""" + adjusted_div = div / shock + return adjusted_div + + +def assert_synchronized_model( + packet: Optional[ModelResultPack] = None, + *, + # Hard-required guiding attributes (per your instruction) + symbol: str, + undo_adjust: bool, + dividend_type: DivType, + # Optional guiding attributes (enable if you want stricter checks) + series_id: Optional[SeriesId] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + market_model: Optional[OptionPricingModel] = None, + vol_model: Optional[VolatilityModel] = None, + model_price: Optional[ModelPrice] = None, + # Individual results (override packet fields if provided) + spot: Optional[SpotResult] = None, + dividend: Optional[DividendsResult] = None, + rates: Optional[RatesResult] = None, + forward: Optional[ForwardResult] = None, + option_spot: Optional[OptionSpotResult] = None, + vol: Optional[VolatilityResult] = None, + greek: Optional[GreekResultSet] = None, + # Point in time check + is_rt: bool = True, + check_fallback_option: bool = False, + # Alignment policy + anchor: str = "option_spot_midpoint", + require_anchor: bool = True, +) -> None: + """ + Authoritative synchronization checks for model inputs. + + Accepts either: + - packet: ModelResultPack + - and/or individual result overrides (spot=..., dividend=..., ...) + + Hard requirements (must not be None): + - symbol: non-empty string + - undo_adjust: bool + - dividend_type: DivType + + Skips any individual result object that is None. + + Checks: + 1) Symbol consistency across all present results (allows result.symbol=None) + 2) Dividend type consistency across (dividend, forward, vol, packet.dividend_type) + 3) undo_adjust consistency across (spot, dividend, forward, packet.undo_adjust) + 4) Canon hard contract: dividend.undo_adjust must equal undo_adjust when dividend exists + 5) Date/index alignment: anchored on option_spot.midpoint by default + """ + + # ------------------------- + # 0) Validate required args + # ------------------------- + if symbol is None or not isinstance(symbol, str) or not symbol.strip(): + raise ValueError("assert_synchronized_model: `symbol` must be a non-empty string.") + if undo_adjust is None or not isinstance(undo_adjust, bool): + raise ValueError("assert_synchronized_model: `undo_adjust` must be a bool (True/False).") + if dividend_type is None: + raise ValueError("assert_synchronized_model: `dividend_type` must not be None.") + + # ------------------------- + # 1) Load from packet first + # ------------------------- + if packet is not None: + if spot is None: + spot = packet.spot + if dividend is None: + dividend = packet.dividend + if rates is None: + rates = packet.rates + if forward is None: + forward = packet.forward + if option_spot is None: + option_spot = packet.option_spot + if vol is None: + vol = packet.vol + if greek is None: + greek = packet.greek + + # Optional strictness knobs (only check if caller passed them) + # If caller provided series_id/endpoint_source etc, verify packet matches. + if series_id is not None and packet.series_id is not None and packet.series_id != series_id: + raise ValueError(f"series_id mismatch: expected {series_id}, packet has {packet.series_id}") + if ( + endpoint_source is not None + and packet.endpoint_source is not None + and packet.endpoint_source != endpoint_source + ): + raise ValueError( + f"endpoint_source mismatch: expected {endpoint_source}, packet has {packet.endpoint_source}" + ) + + # If packet carries dividend_type and caller supplied dividend_type, enforce. + if packet.dividend_type is not None and packet.dividend_type != dividend_type: + raise ValueError(f"dividend_type mismatch: expected {dividend_type}, packet has {packet.dividend_type}") + + # If packet carries undo_adjust and caller supplied undo_adjust, enforce. + if packet.undo_adjust is not None and packet.undo_adjust != undo_adjust: + raise ValueError(f"undo_adjust mismatch: expected {undo_adjust}, packet has {packet.undo_adjust}") + + results = { + "spot": spot, + "dividend": dividend, + "rates": rates, + "forward": forward, + "option_spot": option_spot, + "vol": vol, + "greek": greek, + } + + dividend_factors = [ + "dividend", + "forward", + "vol", + "greek", + ] + + vol_model_factors = [ + "vol", + "greek", + ] + + market_model_factors = [ + "vol", + "greek", + ] + + undo_adjust_factors = [ + "spot", + "dividend", + "forward", + "greek", + "vol", + ] + + model_price_factors = ["vol", "option_spot", "greek"] + + fallback_option_factors = [ + "spot", + "dividend", + "rates", + "forward", + "vol", + "greek", + ] + + rt_factors = [ + "spot", + "dividend", + "rates", + "forward", + "option_spot", + "vol", + "greek", + ] + + # ------------------------- + # 2) Symbol consistency + # ------------------------- + for name, res in results.items(): + if res is None or name == "rates": + continue + res_sym = getattr(res, "symbol", None) + if res_sym is None: + continue + if res_sym != symbol: + raise ValueError(f"Symbol mismatch: expected symbol={symbol}, but {name}.symbol={res_sym}") + + # ------------------------- + # 3) Dividend type consistency + # ------------------------- + + # Generic dividend_type checks + if dividend is not None: + # Loop through all results that have dividend_type attribute + for name in dividend_factors: + res = results.get(name) + if res is None: + continue + res_div_type = getattr(res, "dividend_type", None) + if res_div_type is None: + raise ValueError(f"{name} missing dividend_type attribute.") + if res_div_type != dividend_type: + raise ValueError(f"Dividend type mismatch: expected {dividend_type}, {name} has {res_div_type}") + + # Generic vol_model checks + if vol_model is not None: + for name in vol_model_factors: + res = results.get(name) + if res is None: + continue + res_vol_model = getattr(res, "vol_model", None) + if vol_model is not None and res_vol_model is not None and res_vol_model != vol_model: + raise ValueError(f"vol_model mismatch: expected {vol_model}, {name} has {res_vol_model}") + + # Generic market_model checks + if market_model is not None: + for name in market_model_factors: + res = results.get(name) + if res is None: + continue + res_market_model = getattr(res, "market_model", None) + if market_model is not None and res_market_model is not None and res_market_model != market_model: + raise ValueError(f"market_model mismatch: expected {market_model}, {name} has {res_market_model}") + + # Generic model_price checks + if model_price is not None: + for name in model_price_factors: + res = results.get(name) + if res is None: + print("Skipping, result is None") + continue + res_model_price = getattr(res, "model_price", None) + if res_model_price is None or res_model_price != model_price: + raise ValueError(f"model_price mismatch: expected {model_price}, {name} has {res_model_price}") + + # ------------------------- + # 4) undo_adjust consistency + canon hard contract + # ------------------------- + for name in undo_adjust_factors: + res = results.get(name) + if res is None: + continue + res_undo_adjust = getattr(res, "undo_adjust", None) + if res_undo_adjust is None: + raise ValueError(f"{name} missing undo_adjust attribute.") + if res_undo_adjust != undo_adjust: + raise ValueError(f"undo_adjust mismatch: expected {undo_adjust}, {name} has {res_undo_adjust}") + + if is_rt: + for name in rt_factors: + res = results.get(name) + if res is None: + continue + res_rt = getattr(res, "rt", None) + if res_rt is None: + raise ValueError(f"{name} missing rt attribute.") + if res_rt != is_rt: + raise ValueError(f"rt mismatch: expected {is_rt}, {name} has {res_rt}") + + if check_fallback_option: + for name in fallback_option_factors: + res = results.get(name) + if res is None: + continue + res_fallback_option = getattr(res, "fallback_option", None) + if res_fallback_option is None: + raise ValueError(f"{name} missing fallback_option attribute.") + if not res_fallback_option: + raise ValueError(f"fallback_option mismatch: expected True, {name} has {res_fallback_option}") + + # ------------------------- + # 5) Timeseries alignment checks + # ------------------------- + def _assert_dt_index(x: Union[pd.Series, pd.DataFrame], label: str) -> pd.DatetimeIndex: + if not isinstance(x.index, pd.DatetimeIndex): + raise TypeError(f"{label} index must be DatetimeIndex; got {type(x.index)}") + if not x.index.is_monotonic_increasing: + raise ValueError(f"{label} index must be sorted increasing.") + return x.index + + for name, res in results.items(): + if res is None: + continue + if res.timeseries is None: + raise ValueError(f"{name} timeseries is None.") + _assert_dt_index(res.timeseries, name) + + series_map = { + "spot": spot.timeseries if spot is not None else None, + "dividend": dividend.timeseries if dividend is not None else None, + "rates": rates.daily_risk_free_rates if rates is not None else None, + "forward": forward.timeseries if forward is not None else None, + "option_spot_midpoint": option_spot.timeseries if option_spot is not None else None, + "vol": vol.timeseries if vol is not None else None, + } + + # Determine anchor + if anchor not in series_map: + raise ValueError(f"Unknown anchor='{anchor}'. Valid anchors: {list(series_map.keys())}") + + anchor_series = series_map[anchor] + if require_anchor: + if anchor_series is None: + raise ValueError(f"Anchor '{anchor}' is None but require_anchor=True.") + if isinstance(anchor_series, pd.Series) and anchor_series.empty: + raise ValueError(f"Anchor '{anchor}' is empty but require_anchor=True.") + if isinstance(anchor_series, pd.DataFrame) and anchor_series.empty: + raise ValueError(f"Anchor '{anchor}' is empty but require_anchor=True.") + + # If no anchor (require_anchor=False) and no series, nothing to check. + if anchor_series is None: + return + + anchor_idx = _assert_dt_index(anchor_series, anchor) + + # Require overlap (intersection) with anchor for all other present series + for name, s in series_map.items(): + if name == anchor or s is None: + continue + + # empty is allowed (you may want to tighten this later) + if isinstance(s, (pd.Series, pd.DataFrame)) and s.empty: + continue + + idx = _assert_dt_index(s, name) + inter = idx.intersection(anchor_idx) + if len(inter) == 0: + raise ValueError( + f"Index intersection empty: '{name}' has [{idx.min().date()}..{idx.max().date()}], " + f"anchor '{anchor}' has [{anchor_idx.min().date()}..{anchor_idx.max().date()}]." + ) + + # Optional: global intersection check for vectorized kernels + common = None + for _, s in series_map.items(): + if s is None or (isinstance(s, (pd.Series, pd.DataFrame)) and s.empty): + continue + idx = s.index + common = idx if common is None else common.intersection(idx) + + if common is not None and len(common) == 0: + raise ValueError( + "All detected non-empty timeseries have an empty global index intersection. " + f"Non-empty series: {[k for k,v in series_map.items() if isinstance(v,(pd.Series,pd.DataFrame)) and not v.empty]}" + ) + + +def _load_model_data_timeseries(load_request: LoadRequest) -> ModelResultPack: + """ + Loads model data based on the provided load request. + + Parameters: + load_request (LoadRequest): The request specifying what data to load. + + Returns: + ModelResultPack: A container with the loaded model data. + """ + ## Import here to avoid circular dependencies + from trade.datamanager.dividend import DividendDataManager + from trade.datamanager.rates import RatesDataManager + from trade.datamanager.spot import SpotDataManager + from trade.datamanager.forward import ForwardDataManager + from trade.datamanager.option_spot import OptionSpotDataManager + from trade.datamanager.vol import VolDataManager + from trade.datamanager.greeks import GreekDataManager + + is_as_of = load_request.on_date + is_rt = load_request.rt + load_info = {} + start_time = time.time() + packet = DateRangePacket( + start_date=load_request.start_date, end_date=load_request.end_date, maturity_date=load_request.expiration + ) + load_info["date_range_packet"] = time.time() - start_time + symbol = load_request.symbol + start_date = packet.start_date + end_date = packet.end_date + expiration = packet.maturity_date + d = load_request.load_dividend + r = load_request.load_rates + s = load_request.load_spot + f = load_request.load_forward + vol = load_request.load_vol + opt_spot = load_request.load_option_spot + greek = load_request.load_greek + model_price = load_request.model_price + dividend_type = load_request.dividend_type or OptionDataConfig().dividend_type + D, R, S, F, V, G, OPTION_SPOT = ( + load_request.dividend_timeseries, + load_request.rates_timeseries, + load_request.spot_timeseries, + load_request.forward_timeseries, + load_request.vol_timeseries, + load_request.greek_timeseries, + load_request.option_spot_timeseries, + ) + + model_data = ModelResultPack() + + # Load BSM-specific data + if d: + start_time = time.time() + d_params = { + "maturity_date": expiration, + "dividend_type": dividend_type, + "undo_adjust": load_request.undo_adjust, + } + if not is_as_of and not is_rt: + D = DividendDataManager(symbol).get_schedule_timeseries( + start_date=start_date, + end_date=end_date, + **d_params, + ) + elif is_as_of and not is_rt: + D = DividendDataManager(symbol).get_schedule( + valuation_date=end_date, + fallback_option=load_request.fall_back_option, + **d_params, + ) + else: # is_rt + D = DividendDataManager(symbol).rt( + fallback_option=load_request.fall_back_option, + **d_params, + ) + load_info["dividend_load_time"] = time.time() - start_time + + if r: + start_time = time.time() + if not is_as_of and not is_rt: + R = RatesDataManager().get_risk_free_rate_timeseries(start_date=start_date, end_date=end_date) + elif is_as_of and not is_rt: + R = RatesDataManager().get_rate(date=end_date, fallback_option=load_request.fall_back_option) + else: # is_rt + R = RatesDataManager().rt(fallback_option=load_request.fall_back_option) + load_info["rates_load_time"] = time.time() - start_time + + if s: + start_time = time.time() + if not is_as_of and not is_rt: + S = SpotDataManager(symbol).get_spot_timeseries( + start_date=start_date, end_date=end_date, undo_adjust=load_request.undo_adjust + ) + elif is_as_of and not is_rt: + S = SpotDataManager(symbol).get_at_time(date=end_date) + else: # is_rt + S = SpotDataManager(symbol=symbol).rt(fallback_option=load_request.fall_back_option) + load_info["spot_load_time"] = time.time() - start_time + + if f: + start_time = time.time() + f_params = { + "maturity_date": expiration, + "use_chain_spot": load_request.undo_adjust, + "dividend_type": dividend_type, + "dividend_result": D, + "spot": S, + "rates": R, + } + if not is_as_of and not is_rt: + F = ForwardDataManager(symbol=symbol).get_forward_timeseries( + start_date=start_date, + end_date=end_date, + **f_params, + ) + elif is_as_of and not is_rt: + F = ForwardDataManager(symbol=symbol).get_forward( + date=end_date, + **f_params, + ) + else: # is_rt + F = ForwardDataManager(symbol=symbol).rt( + fallback_option=load_request.fall_back_option, + **f_params, + ) + load_info["forward_load_time"] = time.time() - start_time + + if opt_spot: + start_time = time.time() + opt_params = { + "expiration": expiration, + "strike": load_request.strike, + "right": load_request.right, + "endpoint_source": load_request.endpoint_source, + "model_price": model_price, + } + if not is_as_of and not is_rt: + market_price = OptionSpotDataManager(symbol=symbol).get_option_spot_timeseries( + start_date=start_date, + end_date=end_date, + **opt_params, + ) + elif is_as_of and not is_rt: + market_price = OptionSpotDataManager(symbol=symbol).get_option_spot( + date=end_date, + **opt_params, + ) + else: # is_rt + opt_params.pop("endpoint_source") # RT does not use endpoint_source + opt_params.pop("model_price") # RT does not use model_price + market_price = OptionSpotDataManager(symbol=symbol).rt( + **opt_params, + ) + + load_info["option_spot_load_time"] = time.time() - start_time + OPTION_SPOT = market_price + + if vol: + start_time = time.time() + v_params = { + "expiration": expiration, + "strike": load_request.strike, + "right": load_request.right, + "market_model": load_request.market_model, + "vol_model": load_request.vol_model, + "dividends": D, + "F": F, + "S": S, + "r": R, + "dividend_type": dividend_type, + "market_price": OPTION_SPOT, + "undo_adjust": load_request.undo_adjust, + "endpoint_source": load_request.endpoint_source, + "model_price": model_price, + } + if not is_as_of and not is_rt: + V = VolDataManager(symbol=symbol).get_implied_volatility_timeseries( + start_date=start_date, + end_date=end_date, + **v_params, + ) + elif is_as_of and not is_rt: + V = VolDataManager(symbol=symbol).get_at_time_implied_volatility( + as_of=end_date, + fallback_option=load_request.fall_back_option, + **v_params, + ) + else: # is_rt + v_params.pop("endpoint_source") # RT does not use endpoint_source + V = VolDataManager(symbol=symbol).rt( + fallback_option=load_request.fall_back_option, + **v_params, + ) + + load_info["vol_load_time"] = time.time() - start_time + model_data.vol = V + if greek: + start_time = time.time() + grk_params = { + "expiration": expiration, + "strike": load_request.strike, + "right": load_request.right, + "market_model": load_request.market_model, + "d": D, + "f": F, + "S": S, + "r": R, + "vol": V, + "dividend_type": dividend_type, + "undo_adjust": load_request.undo_adjust, + "endpoint_source": load_request.endpoint_source, + "model_price": model_price, + } + if not is_as_of and not is_rt: + G = GreekDataManager(symbol=symbol).get_greeks_timeseries( + start_date=start_date, + end_date=end_date, + **grk_params, + ) + elif is_as_of and not is_rt: + G = GreekDataManager(symbol=symbol).get_at_time_greeks( + as_of=end_date, + fallback_option=load_request.fall_back_option, + **grk_params, + ) + else: # is_rt + grk_params.pop("endpoint_source") # RT does not use endpoint_source + G = GreekDataManager(symbol=symbol).rt( + fallback_option=load_request.fall_back_option, + **grk_params, + ) + load_info["greek_load_time"] = time.time() - start_time + model_data.greek = G + + model_data.dividend = D + model_data.dividend_type = dividend_type + model_data.forward = F + model_data.rates = R + model_data.spot = S + model_data.option_spot = OPTION_SPOT + model_data.series_id = SeriesId.HIST if (not is_as_of and not is_rt) else SeriesId.AT_TIME + model_data.undo_adjust = load_request.undo_adjust + model_data.time_to_load = load_info + model_data.endpoint_source = load_request.endpoint_source + + ## Log what was loaded + log_model_load_info( + log_info=load_info, + is_rt=is_rt, + is_timeseries=not is_as_of, + symbol=symbol, + expiration=expiration, + strike=load_request.strike, + right=load_request.right, + dividend_type=dividend_type, + market_model=load_request.market_model.name if load_request.market_model else "N/A", + ) + + if not any( + [ + load_request.load_dividend, + load_request.load_rates, + load_request.load_spot, + load_request.load_forward, + load_request.load_option_spot, + load_request.load_vol, + load_request.load_greek, + ] + ): + logger.info(("No data requested to load in _load_model_data_timeseries()." + f" Option: Symbol={symbol}, exp={expiration}, strike={load_request.strike} right={load_request.right}" + f" Load bools: d={d}, r={r}, s={s}, f={f}, opt_spot={opt_spot}, vol={vol}, greek={greek}")) + return model_data + + assert_synchronized_model( + packet=model_data, + symbol=symbol, + undo_adjust=load_request.undo_adjust, + dividend_type=dividend_type, + require_anchor=model_data.option_spot is not None, + is_rt=is_rt, + check_fallback_option=is_rt or is_as_of, + ) + + return model_data diff --git a/trade/datamanager/utils/vol_helpers.py b/trade/datamanager/utils/vol_helpers.py new file mode 100644 index 0000000..f3f7982 --- /dev/null +++ b/trade/datamanager/utils/vol_helpers.py @@ -0,0 +1,187 @@ +from datetime import datetime +from typing import Any, Optional, Tuple, List +import pandas as pd +from trade.datamanager.result import ( + DividendsResult, + VolatilityResult, + SpotResult, + ForwardResult, + RatesResult, + OptionSpotResult, + ModelResultPack, +) +from trade.datamanager.utils.date import sync_date_index, time_distance_helper, _sync_date +from trade.datamanager.base import BaseDataManager +from trade.datamanager._enums import OptionSpotEndpointSource +from trade.datamanager.utils.cache import ( + _check_cache_for_timeseries_data_structure, + _data_structure_cache_it, +) +from trade.helpers.helper import to_datetime +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.data_structure import _data_structure_sanitize +from trade.optionlib.config.types import DivType +from trade.optionlib.assets.dividend import vector_convert_to_time_frac +from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME + +logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) + + +def _prepare_vol_calculation_setup( + manager: BaseDataManager, + start_date: str, + end_date: str, + expiration: str, + strike: float, + right: str, + dividend_type: Optional[DivType], + endpoint_source: Optional[OptionSpotEndpointSource], + result: Optional[VolatilityResult] = None, +) -> Tuple[VolatilityResult, DivType, OptionSpotEndpointSource, str, str, datetime, datetime]: + """Prepare common setup for volatility calculations.""" + result = VolatilityResult() if result is None else result + dividend_type = dividend_type or manager.CONFIG.dividend_type + endpoint_source = endpoint_source or manager.CONFIG.option_spot_endpoint_source + + start_date, end_date = _sync_date( + symbol=manager.symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + right=right, + strike=strike, + endpoint_source=endpoint_source, + ) + + start_str = to_datetime(start_date).strftime("%Y-%m-%d") + end_str = to_datetime(end_date).strftime("%Y-%m-%d") + + return result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date + + +def _handle_cache_for_vol( + manager: BaseDataManager, + key: str, + start_date: datetime, + end_date: datetime, + result: VolatilityResult, + optional_name: Optional[str] = "vol" +) -> Tuple[Optional[pd.Series], bool, datetime, datetime, Optional[VolatilityResult]]: + """Handle cache checking logic for volatility calculations. + + Returns: + Tuple of (cached_data, is_partial, adjusted_start, adjusted_end, result_or_none) + If result_or_none is not None, caller should return it immediately (full cache hit) + """ + cached_data, is_partial, start_date, end_date = _check_cache_for_timeseries_data_structure( + key=key, self=manager, start_dt=start_date, end_dt=end_date + ) + + if cached_data is not None and not is_partial: + logger.info(f"Cache hit for {optional_name} timeseries key: {key}") + result.timeseries = cached_data + return cached_data, is_partial, start_date, end_date, result + elif is_partial: + logger.info(f"Cache partially covers requested date range. Key: {key}. Fetching missing dates.") + else: + logger.info(f"No cache found for key: {key}. Fetching from source.") + + return cached_data, is_partial, start_date, end_date, None + + +def _merge_and_cache_vol_result( + manager: BaseDataManager, + iv_timeseries: pd.Series, + cached_data: Optional[pd.Series], + is_partial: bool, + key: str, + start_str: str, + end_str: str, +) -> pd.Series: + """Merge with cache if partial, cache result, and sanitize.""" + # Merge with cached data if partial + if cached_data is not None and is_partial: + merged = pd.concat([cached_data, iv_timeseries]) + iv_timeseries = merged[~merged.index.duplicated(keep="last")].sort_index() + + # Cache the fetched data + _data_structure_cache_it(manager, key, iv_timeseries) + + # Sanitize before returning + iv_timeseries = _data_structure_sanitize( + iv_timeseries, + start=start_str, + end=end_str, + source_name=f"final {key} timeseries after merging cache and fetched data.", + ) + + return iv_timeseries + + +def _merge_provided_with_loaded_data( + model_data: "ModelResultPack", + S: Optional[SpotResult] = None, + F: Optional[ForwardResult] = None, + r: Optional[RatesResult] = None, + dividends: Optional[DividendsResult] = None, + market_price: Optional[OptionSpotResult] = None, +) -> Tuple[ + Optional[SpotResult], Optional[ForwardResult], RatesResult, Optional[DividendsResult], Optional[OptionSpotResult] +]: + """Merge user-provided data with loaded data, prioritizing provided data.""" + S = S if S is not None else model_data.spot + F = F if F is not None else model_data.forward + r = r if r is not None else model_data.rates + dividends = dividends if dividends is not None else model_data.dividend + market_price = market_price if market_price is not None else model_data.option_spot + + # Update model_data for consistency + if S is not None: + model_data.spot = S + if F is not None: + model_data.forward = F + if r is not None: + model_data.rates = r + if dividends is not None: + model_data.dividend = dividends + if market_price is not None: + model_data.option_spot = market_price + + return S, F, r, dividends, market_price + + +def _prepare_dividend_data_for_pricing( + dividends: DividendsResult, + dividend_type: DivType, + expiration: str, + *data_to_sync: pd.Series, +) -> Tuple[Any, ...]: + """Prepare dividend data and synchronize all series. + + Returns: + Tuple of synchronized series (including prepared dividends as last element) + """ + if dividend_type == DivType.DISCRETE: + dividends_ts = dividends.daily_discrete_dividends + synced = sync_date_index(*data_to_sync, dividends_ts) + + # Convert to time fractions + dividends_prepared = vector_convert_to_time_frac( + schedules=synced[-1], + valuation_dates=synced[0].index, + end_dates=[to_datetime(expiration)] * len(synced[0].index), + ) + return (*synced[:-1], dividends_prepared) + + elif dividend_type == DivType.CONTINUOUS: + dividends_ts = dividends.daily_continuous_dividends + synced = sync_date_index(*data_to_sync, dividends_ts) + return synced + + +def _prepare_time_to_expiration( + date_index: pd.DatetimeIndex, + expiration: str, +) -> List[float]: + """Calculate time to expiration for each date in the index.""" + return [time_distance_helper(x, expiration) for x in date_index] diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py new file mode 100644 index 0000000..ae4d3a1 --- /dev/null +++ b/trade/datamanager/vars.py @@ -0,0 +1,98 @@ +from pathlib import Path +import os +import pandas as pd +from datetime import time +from datetime import datetime +from typing import List, Dict, Any +from trade.helpers.Logging import setup_logger +from typing import TYPE_CHECKING +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE +from trade.datamanager.utils.logging import get_logging_level +from trade import register_signal +import signal +if TYPE_CHECKING: + from trade.datamanager.market_data import MarketTimeseries +logger = setup_logger("trade.datamanager.vars", stream_log_level=get_logging_level()) + +DM_GEN_PATH = Path(os.getenv("GEN_CACHE_PATH")) / "dm_gen_cache" +TS: "MarketTimeseries" = None # type: MarketTimeseries +_LOG_TO_DISK_BUCKET : List[Dict[str, Any]] = [] +LOADED_NAMES = set() +MARKET_OPEN_TIME = time(9, 30) +MARKET_CLOSE_TIME = time(16, 0) +DEFAULT_SCENARIOS = [0.9, 0.95, 1.0, 1.05, 1.1] +DEFAULT_VOL_SCENARIOS = [-0.02, -0.01, 0.0, 0.01, 0.02] + + +def set_times_series()-> "MarketTimeseries": + from trade.datamanager.market_data import MarketTimeseries + global TS + TS = MarketTimeseries(_end=datetime.now(), _start=OPTION_TIMESERIES_START_DATE) + return TS + +def get_times_series() -> "MarketTimeseries": + global TS + if TS is None: + set_times_series() + return TS + +def send_log_to_disk() -> None: + global _LOG_TO_DISK_BUCKET + if not _LOG_TO_DISK_BUCKET: + logger.info("No logs to write to disk.") + return + log_path = DM_GEN_PATH / "dm_runtime_logs.csv" + df = pd.DataFrame(_LOG_TO_DISK_BUCKET) + if log_path.exists(): + df_existing = pd.read_csv(log_path) + df = pd.concat([df_existing, df], ignore_index=True) + df.to_csv(log_path, index=False) + logger.info(f"Wrote {_LOG_TO_DISK_BUCKET.__len__()} log entries to disk at {log_path}.") + _LOG_TO_DISK_BUCKET.clear() + +def add_to_log_bucket(entry: Dict[str, Any]) -> None: + global _LOG_TO_DISK_BUCKET + _LOG_TO_DISK_BUCKET.append(entry) + +register_signal("exit", send_log_to_disk) +register_signal(signal.SIGINT, send_log_to_disk) +register_signal(signal.SIGTERM, send_log_to_disk) + +def load_name(symbol: str): + key = (symbol, datetime.now().date()) + global LOADED_NAMES + if key not in LOADED_NAMES: + logger.info(f"Loading timeseries for {symbol}...") + get_times_series().load_timeseries(symbol, start_date=OPTION_TIMESERIES_START_DATE, end_date=datetime.now()) + LOADED_NAMES.add(key) + else: + logger.info(f"Timeseries for {symbol} already loaded.") + +def is_name_loaded(symbol: str) -> bool: + global LOADED_NAMES + key = (symbol, datetime.now().date()) + return key in LOADED_NAMES + +def clear_loaded_names(): + global LOADED_NAMES + LOADED_NAMES.clear() + logger.info("Cleared loaded names cache.") + + +def get_loaded_names() -> set: + global LOADED_NAMES + return LOADED_NAMES + + + +## This is the time after which today data can be cached. Any time before this time, +## today data should be reloaded to ensure completeness. +TODAY_RELOAD_CUTOFF = time(18, 30) # 6:30 PM + +## This is the minimum time before real-time data is requested +## ie if current time is before this time, real-time will not query for today and instead +## rely on RealTimeFallback option +MIN_TIME_BEFORE_REAL_TIME = time(9, 45) # 9:45 AM + +## This is done to avoid circular import issues. MarketTimeseries is the main class responsible +set_times_series() \ No newline at end of file diff --git a/trade/datamanager/vol.py b/trade/datamanager/vol.py new file mode 100644 index 0000000..0faca20 --- /dev/null +++ b/trade/datamanager/vol.py @@ -0,0 +1,1054 @@ +"""Volatility data manager for computing implied volatilities from option market prices. + +This module provides the VolDataManager class for calculating implied volatilities using +various pricing models (Black-Scholes-Merton, Cox-Ross-Rubinstein binomial, European +equivalent). It handles the complete workflow including data loading, caching, model +selection, and result formatting. + +Key Features: + - Multiple pricing models: BSM, CRR binomial, European equivalent + - Support for American and European exercise styles + - Discrete and continuous dividend treatments + - Automatic data loading and caching + - Real-time and historical volatility calculation + - Singleton pattern per symbol for efficient resource management + +Typical Usage: + >>> from trade.datamanager.vol import VolDataManager + >>> from trade.optionlib.config.types import DivType + >>> + >>> # Initialize manager for AAPL + >>> vol_mgr = VolDataManager("AAPL") + >>> + >>> # Get implied volatilities for an option + >>> result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE, + ... american=True + ... ) + >>> print(result.timeseries.head()) +""" +from datetime import datetime +from typing import Any, ClassVar, Optional +import pandas as pd +from trade.datamanager._enums import ( + ArtifactType, + Interval, + ModelPrice, + OptionPricingModel, + RealTimeFallbackOption, + VolatilityModel, + SeriesId, + OptionSpotEndpointSource, +) +from trade.datamanager.base import BaseDataManager, CacheSpec +from trade.datamanager.config import OptionDataConfig +from trade.datamanager.requests import LoadRequest +from trade.datamanager.result import ( + VolatilityResult, + ForwardResult, + RatesResult, + OptionSpotResult, + SpotResult, + DividendsResult, +) +from trade.datamanager.utils.vol_helpers import ( + _prepare_time_to_expiration, + _handle_cache_for_vol, + _merge_provided_with_loaded_data, + _prepare_dividend_data_for_pricing, + _merge_and_cache_vol_result, + _prepare_vol_calculation_setup, +) +from trade.datamanager.utils.model import _load_model_data_timeseries +from trade.optionlib.vol.implied_vol import vector_bsm_iv_estimation, vector_crr_iv_estimation +from trade.optionlib.pricing.binomial import vector_crr_binomial_pricing +from trade.optionlib.config.types import DivType +from trade.helpers.helper import change_to_last_busday, to_datetime +from trade.helpers.Logging import setup_logger +from trade.datamanager.utils.date import is_available_on_date, sync_date_index +from trade.datamanager.utils.logging import get_logging_level +from trade.optionlib.assets.dividend import vector_convert_to_time_frac + +logger = setup_logger("trade.datamanager.vol", stream_log_level=get_logging_level()) + +class VolDataManager(BaseDataManager): + """Manager for computing and caching implied volatilities from option market prices. + + Singleton class (per symbol) that orchestrates the computation of implied volatilities + using various option pricing models. Automatically loads required market data (spot, + forward, rates, dividends, option prices) and caches results for efficient reuse. + + Supports three pricing approaches: + 1. Black-Scholes-Merton (BSM) - Fast, European options only + 2. Cox-Ross-Rubinstein (CRR) - Binomial tree, supports American exercise + 3. European Equivalent (EURO_EQIV) - Converts American IVs to European equivalent + + Attributes: + CACHE_NAME: Cache identifier for volatility data. + DEFAULT_SERIES_ID: Default series identifier (historical data). + CONFIG: Configuration object with default settings for pricing models. + INSTANCES: Class-level dict maintaining singleton instances per symbol. + symbol: Ticker symbol for the underlying asset. + + Examples: + >>> # Basic usage with BSM model + >>> vol_mgr = VolDataManager("AAPL") + >>> result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... model=OptionPricingModel.BSM + ... ) + + >>> # American option with CRR binomial + >>> result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="p", + ... american=True, + ... model=OptionPricingModel.BINOMIAL, + ... n_steps=200 + ... ) + + >>> # Real-time volatility + >>> rt_vol = vol_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + """ + CACHE_NAME: ClassVar[str] = "vol_data_manager_cache" + DEFAULT_SERIES_ID: ClassVar["SeriesId"] = SeriesId.HIST + CONFIG: OptionDataConfig = OptionDataConfig() + CACHE_SPEC: CacheSpec = CacheSpec(cache_fname=CACHE_NAME) + INSTANCES: ClassVar[dict[str, "VolDataManager"]] = {} + + def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> "VolDataManager": + if symbol not in cls.INSTANCES: + instance = object.__new__(cls) + cls.INSTANCES[symbol] = instance + return cls.INSTANCES[symbol] + + def __init__( + self, symbol: str, *, enable_namespacing: bool = False + ) -> None: + """Initialize VolDataManager with symbol-specific configuration. + + Args: + symbol: Ticker symbol for the underlying asset (e.g., "AAPL", "MSFT"). + enable_namespacing: If True, enables namespace prefixing for cache keys. + + Examples: + >>> # Basic initialization + >>> vol_mgr = VolDataManager("AAPL") + + >>> # With custom cache settings + >>> from trade.datamanager.base import CacheSpec + >>> cache_spec = CacheSpec( + ... default_expire_days=365, + ... cache_fname="custom_vol_cache" + ... ) + >>> vol_mgr = VolDataManager("AAPL", cache_spec=cache_spec) + """ + self.symbol = symbol + + if getattr(self, "_initialized", False): + return + self._initialized = True + super().__init__( + enable_namespacing=enable_namespacing, + symbol=symbol, + ) + + def _get_bsm_implied_volatility_timeseries( + self, + start_date: str, + end_date: str, + expiration: str, + strike: float, + right: str, + dividend_type: Optional[DivType] = DivType.DISCRETE, + *, + result: Optional[VolatilityResult] = None, + F: Optional[ForwardResult] = None, + r: Optional[RatesResult] = None, + market_price: Optional[OptionSpotResult] = None, + model_price: Optional[ModelPrice] = None, + undo_adjust: bool = True, + ) -> VolatilityResult: + """Compute implied volatilities using Black-Scholes-Merton model. + + Internal method that calculates daily implied volatilities by matching market prices + to BSM prices. Automatically loads required data (forward, rates, option prices) if + not provided. Uses caching to avoid redundant computations. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + result: Optional pre-initialized VolatilityResult container. + F: Optional pre-computed forward prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + market_price: Optional pre-computed option market prices. If None, loads automatically. + undo_adjust: If True, uses split-adjusted prices. + model_price: Optional[ModelPrice] = None, + Specifies which price to use from option spot data (CLOSE, OPEN, MIDPOINT). + If None, defaults to CONFIG.model_price. + Returns: + VolatilityResult containing daily implied volatilities with DatetimeIndex, + model metadata, and cache key. + + Examples: + >>> # Internal usage - typically called via get_implied_volatility_timeseries + >>> result = vol_mgr._get_bsm_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... dividend_type=DivType.DISCRETE + ... ) + """ + + # Use utility: Prepare setup + endpoint_source = result.endpoint_source if result is not None else self.CONFIG.option_spot_endpoint_source + result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = _prepare_vol_calculation_setup( + self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result + ) + + # Make key for caching + key = self.make_key( + symbol=self.symbol, + interval=Interval.EOD, + artifact_type=ArtifactType.IV, + series_id=SeriesId.HIST, + option_pricing_model=OptionPricingModel.BSM, + volatility_model=VolatilityModel.MARKET, + model_price=model_price or self.CONFIG.model_price, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + expiration=expiration, + strike=strike, + right=right, + ) + + # Use utility: Handle cache + cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol( + self, key, start_date, end_date, result + ) + if early_return is not None: + return early_return + + # Load model data + load_request = LoadRequest( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + dividend_type=dividend_type, + load_spot=False, + load_forward=F is None, + load_rates=r is None, + load_option_spot=market_price is None, + load_dividend=False, ## Not needed for BSM IV. Already handled in forward. + load_vol=False, + strike=strike, + right=right, + undo_adjust=undo_adjust, + endpoint_source=endpoint_source, + model_price=model_price, + ) + model_data = _load_model_data_timeseries(load_request) + + # Use utility: Merge provided data + _, F, r, _, market_price = _merge_provided_with_loaded_data(model_data, F=F, r=r, market_price=market_price) + + # Extract data + forward = F.daily_continuous_forward if dividend_type == DivType.CONTINUOUS else F.daily_discrete_forward + rates = r.daily_risk_free_rates + option_spot = market_price.price + forward, rates, option_spot = sync_date_index(forward, rates, option_spot) + + # Use utility: Prepare T + T = _prepare_time_to_expiration(forward.index, expiration) + + # Calculate IV + iv_timeseries = vector_bsm_iv_estimation( + F=forward.values, + K=[strike] * len(forward), + T=T, + r=rates.values, + market_price=option_spot.values, + right=[right.lower()] * len(forward), + ) + iv_timeseries = pd.Series(data=iv_timeseries, index=forward.index) + + # Use utility: Merge and cache + iv_timeseries = _merge_and_cache_vol_result( + self, iv_timeseries, cached_data, is_partial, key, start_str, end_str + ) + + # Prepare result + result.timeseries = iv_timeseries + return result + + def _get_crr_implied_volatility_timeseries( + self, + start_date: str, + end_date: str, + expiration: str, + strike: float, + right: str, + dividend_type: Optional[DivType] = DivType.DISCRETE, + american: bool = True, + result: Optional[VolatilityResult] = None, + *, + S: Optional[SpotResult] = None, + r: Optional[RatesResult] = None, + dividends: Optional[DividendsResult] = None, + market_price: Optional[OptionSpotResult] = None, + model_price: Optional[ModelPrice] = None, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + ) -> VolatilityResult: + """Compute implied volatilities using Cox-Ross-Rubinstein binomial model. + + Internal method that calculates daily implied volatilities using CRR binomial trees. + Supports both American and European exercise styles. Automatically loads required + data (spot, rates, dividends, option prices) if not provided. Uses caching for + efficient reuse. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + american: If True, prices American exercise; if False, European. + result: Optional pre-initialized VolatilityResult container. + S: Optional pre-computed spot prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + dividends: Optional pre-computed dividend data. If None, loads automatically. + market_price: Optional pre-computed option market prices. If None, loads automatically. + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of time steps in binomial tree. Defaults to CONFIG.n_steps. + + Returns: + VolatilityResult containing daily implied volatilities with DatetimeIndex, + model metadata (BINOMIAL), and cache key. + + Examples: + >>> # Internal usage - typically called via get_implied_volatility_timeseries + >>> result = vol_mgr._get_crr_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="p", + ... american=True, + ... n_steps=200 + ... ) + """ + + # Use utility: Prepare setup + endpoint_source = market_price.endpoint_source if market_price is not None else None + result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = _prepare_vol_calculation_setup( + self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result + ) + n_steps = n_steps or self.CONFIG.n_steps + + # Make key for caching + key = self.make_key( + symbol=self.symbol, + interval=Interval.EOD, + artifact_type=ArtifactType.IV, + series_id=SeriesId.HIST, + option_pricing_model=OptionPricingModel.BINOMIAL, + volatility_model=VolatilityModel.MARKET, + model_price=model_price or self.CONFIG.model_price, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + expiration=expiration, + strike=strike, + right=right, + american=american, + n_steps=n_steps, + ) + result.key = key + + # Use utility: Handle cache + cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol( + self, key, start_date, end_date, result + ) + if early_return is not None: + return early_return + + # Load model data + load_request = LoadRequest( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + dividend_type=dividend_type, + model_price=model_price or self.CONFIG.model_price, + load_spot=S is None, + load_rates=r is None, + load_dividend=dividends is None, + load_option_spot=market_price is None, + load_forward= False, ## Not needed for CRR IV. Spot used directly. + load_vol=False, + strike=strike, + right=right, + undo_adjust=undo_adjust, + endpoint_source=endpoint_source, + ) + model_data = _load_model_data_timeseries(load_request) + + # Use utility: Merge provided data + S, _, r, dividends, market_price = _merge_provided_with_loaded_data( + model_data, S=S, r=r, dividends=dividends, market_price=market_price + ) + + # Extract data + spot = S.daily_spot + rates = r.daily_risk_free_rates + option_spot = market_price.price + + # Use utility: Prepare dividends and sync + spot, rates, option_spot, dividends_ts = _prepare_dividend_data_for_pricing( + dividends, dividend_type, expiration, spot, rates, option_spot + ) + + # Use utility: Prepare T + T = _prepare_time_to_expiration(option_spot.index, expiration) + + # Calculate IV + iv_timeseries = vector_crr_iv_estimation( + S=spot.values, + K=[strike] * len(spot), + T=T, + r=rates.values, + market_price=option_spot.values, + dividends=dividends_ts, + option_type=[right.lower()] * len(spot), + dividend_type=[dividend_type.name.lower()] * len(spot), + american=[american] * len(spot), + N=[n_steps] * len(spot), + ) + iv_timeseries = pd.Series(data=iv_timeseries, index=spot.index) + + # Use utility: Merge and cache + iv_timeseries = _merge_and_cache_vol_result( + self, iv_timeseries, cached_data, is_partial, key, start_str, end_str + ) + + # Prepare result + result.timeseries = iv_timeseries + return result + + def _get_european_equivalent_volatility_timeseries( + self, + start_date: str, + end_date: str, + expiration: str, + strike: float, + right: str, + *, + result: Optional[VolatilityResult] = None, + crr_american_vols: VolatilityResult, + F: Optional[ForwardResult] = None, + r: Optional[RatesResult] = None, + dividends: Optional[DividendsResult] = None, + dividend_type: Optional[DivType] = DivType.DISCRETE, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + ) -> VolatilityResult: + """Convert American implied volatilities to European-equivalent BSM volatilities. + + Internal method that takes CRR American implied volatilities and converts them to + European-equivalent Black-Scholes volatilities. This is done by: + 1. Pricing European options using CRR with American IVs + 2. Solving for BSM volatilities that match those European CRR prices + + This conversion is useful for comparing American option volatilities to European + benchmarks or for further analysis requiring BSM framework. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + result: Optional pre-initialized VolatilityResult container. + crr_american_vols: Pre-computed American implied volatilities from CRR model. + F: Optional pre-computed forward prices. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + dividends: Optional pre-computed dividend data. If None, loads automatically. + dividend_type: Dividend treatment type (DISCRETE or CONTINUOUS). + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of time steps in binomial tree. Defaults to CONFIG.n_steps. + + Returns: + VolatilityResult containing daily European-equivalent implied volatilities + with DatetimeIndex, model metadata (EURO_EQIV), and cache key. + + Examples: + >>> # Internal usage - typically called via get_implied_volatility_timeseries + >>> # First get American IVs + >>> american_vols = vol_mgr._get_crr_implied_volatility_timeseries(...) + >>> # Convert to European equivalent + >>> euro_vols = vol_mgr._get_european_equivalent_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="p", + ... crr_american_vols=american_vols + ... ) + """ + + # Use utility: Prepare setup + endpoint_source = crr_american_vols.endpoint_source + result, dividend_type, endpoint_source, start_str, end_str, start_date, end_date = _prepare_vol_calculation_setup( + self, start_date, end_date, expiration, strike, right, dividend_type, endpoint_source, result + ) + + # Make key for caching + key = self.make_key( + symbol=self.symbol, + interval=Interval.EOD, + artifact_type=ArtifactType.IV, + series_id=SeriesId.HIST, + option_pricing_model=OptionPricingModel.EURO_EQIV, + volatility_model=VolatilityModel.MARKET, + dividend_type=dividend_type, + endpoint_source=endpoint_source, + expiration=expiration, + strike=strike, + right=right, + ) + + # Use utility: Handle cache + cached_data, is_partial, start_date, end_date, early_return = _handle_cache_for_vol( + self, key, start_date, end_date, result + ) + if early_return is not None: + return early_return + + # Load model data + load_request = LoadRequest( + symbol=self.symbol, + start_date=start_date, + end_date=end_date, + expiration=expiration, + dividend_type=dividend_type, + load_spot=True, + load_forward=F is None, + load_rates=r is None, + load_dividend=dividends is None, + strike=strike, + right=right, + undo_adjust=undo_adjust, + endpoint_source=endpoint_source, + ) + model_data = _load_model_data_timeseries(load_request) + + # Use utility: Merge provided data + S, F, r, dividends, _ = _merge_provided_with_loaded_data( + model_data, S=model_data.spot, F=F, r=r, dividends=dividends + ) + + # Extract data + spot = S.daily_spot + forward = F.daily_continuous_forward if dividend_type == DivType.CONTINUOUS else F.daily_discrete_forward + rates = r.daily_risk_free_rates + + # Prepare dividends based on type + if dividend_type == DivType.DISCRETE: + dividends_ts = dividends.daily_discrete_dividends + spot, forward, rates, dividends_ts, crr_american_iv = sync_date_index( + spot, forward, rates, dividends_ts, crr_american_vols.timeseries + ) + dividends_ts = vector_convert_to_time_frac( + schedules=dividends_ts, + valuation_dates=spot.index, + end_dates=[to_datetime(expiration)] * len(spot.index), + ) + dividend_yield = pd.Series(data=0.0, index=spot.index) + elif dividend_type == DivType.CONTINUOUS: + dividends_yield = dividends.daily_continuous_dividends + spot, forward, rates, dividend_yield, crr_american_iv = sync_date_index( + spot, forward, rates, dividends_yield, crr_american_vols.timeseries + ) + dividends_ts = [()] * len(spot) + + # Price with CRR using American IVs in European mode + european_crr_price = vector_crr_binomial_pricing( + S0=spot.values, + K=[strike] * len(spot), + T=_prepare_time_to_expiration(spot.index, expiration), + r=rates.values, + sigma=crr_american_iv.values, + dividend_yield=dividend_yield.values, + dividends=dividends_ts, + right=[right.lower()] * len(spot), + N=[n_steps or self.CONFIG.n_steps] * len(spot), + dividend_type=[dividend_type.name.lower()] * len(spot), + american=[False] * len(spot), + ) + + # Convert to BSM equivalent IV + european_equiv_iv = vector_bsm_iv_estimation( + F=forward.values, + K=[strike] * len(spot), + T=_prepare_time_to_expiration(spot.index, expiration), + r=rates.values, + market_price=european_crr_price, + right=[right.lower()] * len(spot), + ) + european_equiv_iv = pd.Series(data=european_equiv_iv, index=spot.index) + + # Use utility: Merge and cache + european_equiv_iv = _merge_and_cache_vol_result( + self, european_equiv_iv, cached_data, is_partial, key, start_str, end_str + ) + + # Prepare result + result.timeseries = european_equiv_iv + return result + + def get_implied_volatility_timeseries( + self, + start_date: str, + end_date: str, + expiration: str, + strike: float, + right: str, + dividend_type: Optional[DivType] = None, + american: bool = True, + *, + market_model: Optional[OptionPricingModel] = None, + S: Optional[SpotResult] = None, + F: Optional[ForwardResult] = None, + dividends: Optional[DividendsResult] = None, + r: Optional[RatesResult] = None, + market_price: Optional[OptionSpotResult] = None, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + vol_model: Optional[VolatilityModel] = None, + model_price: Optional[ModelPrice] = None, + ) -> VolatilityResult: + """Compute daily implied volatilities for a specific option across a date range. + + Main public method for calculating implied volatility timeseries. Automatically + selects the appropriate pricing model (BSM, CRR, or European equivalent) and + orchestrates data loading, computation, and caching. + + Args: + start_date: First valuation date (YYYY-MM-DD string or datetime). + end_date: Last valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment (DISCRETE or CONTINUOUS). Defaults to DISCRETE. + american: If True, uses American exercise; if False, European. + model: Pricing model to use (BSM, BINOMIAL, EURO_EQIV). Defaults to CONFIG.option_model. + model_price: Pricing model price to use (CLOSE, MIDPOINT). Defaults to CONFIG.model_price. + S: Optional pre-computed spot prices. If None, loads automatically. + F: Optional pre-computed forward prices. If None, loads automatically. + dividends: Optional pre-computed dividend data. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + market_price: Optional pre-computed option prices. If None, loads automatically. + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of binomial tree steps. Only used for BINOMIAL/EURO_EQIV models. + endpoint_source: Data source for option prices (e.g., CHAIN, QUOTE). + vol_model: Volatility model to use (MARKET). Defaults to CONFIG.volatility_model. + model_price: Pricing model price to use (CLOSE, MIDPOINT). Defaults to CONFIG.model_price. + Returns: + VolatilityResult containing: + - timeseries: Daily implied volatilities as pandas Series + - model: Volatility model type (MARKET) + - market_model: Pricing model used (BSM, BINOMIAL, or EURO_EQIV) + - dividend_type: Dividend treatment used + - key: Cache key for result + - model_price: Pricing model price used (CLOSE, MIDPOINT) + Raises: + ValueError: If unsupported pricing model is specified. + + Examples: + >>> # Basic European call with BSM + >>> vol_mgr = VolDataManager("AAPL") + >>> result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... american=False, + ... model=OptionPricingModel.BSM + ... ) + >>> print(result.timeseries.head()) + + >>> # American put with CRR binomial + >>> result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="p", + ... american=True, + ... model=OptionPricingModel.BINOMIAL, + ... n_steps=200, + ... dividend_type=DivType.DISCRETE + ... ) + + >>> # European equivalent from American + >>> result = vol_mgr.get_implied_volatility_timeseries( + ... start_date="2025-01-01", + ... end_date="2025-01-31", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... model=OptionPricingModel.EURO_EQIV + ... ) + """ + # Volatility model (currently only MARKET supported) + vol_model = vol_model or self.CONFIG.volatility_model + + # Load model information + market_model = market_model or self.CONFIG.option_model + if dividend_type is None: + logger.info(f"VolDm Using default dividend type from config: {self.CONFIG.dividend_type}") + else: + logger.info(f"VolDm Using specified dividend type: {dividend_type}") + dividend_type = dividend_type or self.CONFIG.dividend_type + endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + model_price = model_price or self.CONFIG.model_price + logger.info(f"VolDm Using model price: {model_price}") + + # Prepare result container + result = VolatilityResult() + result.symbol = self.symbol + result.expiration = to_datetime(expiration) + result.right = right + result.strike = strike + result.dividend_type = dividend_type + result.vol_model = vol_model + result.endpoint_source = endpoint_source + result.market_model = market_model + result.undo_adjust = undo_adjust + result.model_price = model_price + + if market_model == OptionPricingModel.BSM: + return self._get_bsm_implied_volatility_timeseries( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + F=F, + r=r, + market_price=market_price, + model_price=model_price, + undo_adjust=undo_adjust, + result=result, + ) + elif market_model == OptionPricingModel.BINOMIAL: + return self._get_crr_implied_volatility_timeseries( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + S=S, + r=r, + dividends=dividends, + market_price=market_price, + undo_adjust=undo_adjust, + model_price=model_price, + american=american, + n_steps=n_steps, + result=result, + ) + elif market_model == OptionPricingModel.EURO_EQIV: + # First get the CRR American implied volatilities + crr_american_vols = self._get_crr_implied_volatility_timeseries( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + S=S, + r=r, + dividends=dividends, + market_price=market_price, + undo_adjust=undo_adjust, + american=True, + n_steps=n_steps, + model_price=model_price, + result=result, + ) + return self._get_european_equivalent_volatility_timeseries( + start_date=start_date, + end_date=end_date, + expiration=expiration, + strike=strike, + right=right, + crr_american_vols=crr_american_vols, + F=F, + r=r, + dividends=dividends, + dividend_type=dividend_type, + undo_adjust=undo_adjust, + n_steps=n_steps, + result=result, + ) + else: + raise ValueError(f"Unsupported option pricing model: {market_model}") + + def get_at_time_implied_volatility( + self, + as_of: str, + expiration: str, + strike: float, + right: str, + dividend_type: Optional[DivType] = DivType.DISCRETE, + american: bool = True, + *, + vol_model: Optional[VolatilityModel] = None, + fallback_option: Optional[RealTimeFallbackOption] = None, + market_model: Optional[OptionPricingModel] = None, + S: Optional[SpotResult] = None, + F: Optional[ForwardResult] = None, + dividends: Optional[DividendsResult] = None, + r: Optional[RatesResult] = None, + market_price: Optional[OptionSpotResult] = None, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + endpoint_source: Optional[OptionSpotEndpointSource] = None, + model_price: Optional[ModelPrice] = None, + ) -> VolatilityResult: + """Compute implied volatility at a specific point in time. + + Convenience method that retrieves implied volatility for a single date by calling + get_implied_volatility_timeseries with start_date=end_date=as_of. Useful for + historical backtesting or analysis at specific dates. + + Args: + as_of: Specific valuation date (YYYY-MM-DD string or datetime). + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment (DISCRETE or CONTINUOUS). Defaults to DISCRETE. + american: If True, uses American exercise; if False, European. + market_model: Pricing model to use (BSM, BINOMIAL, EURO_EQIV). Defaults to CONFIG.option_model. + S: Optional pre-computed spot prices. If None, loads automatically. + F: Optional pre-computed forward prices. If None, loads automatically. + dividends: Optional pre-computed dividend data. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + market_price: Optional pre-computed option prices. If None, loads automatically. + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of binomial tree steps. Only used for BINOMIAL/EURO_EQIV models. + endpoint_source: Data source for option prices (e.g., CHAIN, QUOTE). + + Returns: + VolatilityResult with single-row timeseries containing the implied volatility + at the specified date. + + Examples: + >>> # Get IV on a specific historical date + >>> vol_mgr = VolDataManager("AAPL") + >>> result = vol_mgr.get_at_time_implied_volatility( + ... as_of="2025-01-15", + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... american=True + ... ) + >>> print(f"IV on 2025-01-15: {result.timeseries.iloc[0]:.4f}") + + >>> # Use in backtesting loop + >>> for date in backtest_dates: + ... vol_result = vol_mgr.get_at_time_implied_volatility( + ... as_of=date, + ... expiration=expiration, + ... strike=strike, + ... right="p" + ... ) + ... iv_value = vol_result.timeseries.iloc[0] + """ + fallback_option = fallback_option or self.CONFIG.real_time_fallback_option + model_price = model_price or self.CONFIG.model_price + as_of = to_datetime(as_of) + if not is_available_on_date(as_of): + logger.warning( + f"Valuation date {as_of} is not a business day or holiday. Resolving using fallback options {fallback_option}." + ) + if fallback_option == RealTimeFallbackOption.RAISE_ERROR: + raise ValueError(f"Valuation date {as_of} is not a business day or holiday.") + if fallback_option == RealTimeFallbackOption.USE_LAST_AVAILABLE: + ## Move date back to last business day + ## Using only change_to_last_busday assumes input date is not business day or is holiday + ## Which the function would roll back + ## But there's a possibility input date is today's date but before market open + ## In that case we need to move back one more business day + as_of = change_to_last_busday(as_of - pd.tseries.offsets.BDay(1), time_of_day_aware=False) + else: + result = VolatilityResult() + result.timeseries = pd.Series(dtype=float, + index=pd.DatetimeIndex([to_datetime(as_of)]), + values = [float('nan') if fallback_option == RealTimeFallbackOption.NAN else 0.0]) + + result.key = None + result.vol_model = vol_model or self.CONFIG.volatility_model + result.market_model = market_model or self.CONFIG.option_model + result.expiration = to_datetime(expiration) + result.right = right + result.strike = strike + result.endpoint_source = endpoint_source or self.CONFIG.option_spot_endpoint_source + result.dividend_type = dividend_type or self.CONFIG.dividend_type + result.symbol = self.symbol + result.undo_adjust = undo_adjust + result.model_price = model_price + result.fallback_option = fallback_option + + return result + + iv_timeseries = self.get_implied_volatility_timeseries( + start_date=as_of, + end_date=as_of, + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + american=american, + market_model=market_model, + S=S, + F=F, + dividends=dividends, + r=r, + market_price=market_price, + undo_adjust=undo_adjust, + n_steps=n_steps, + endpoint_source=endpoint_source, + vol_model=vol_model, + model_price=model_price, + ) + iv_timeseries.timeseries = iv_timeseries.timeseries.loc[to_datetime(as_of) : to_datetime(as_of)] + iv_timeseries.fallback_option = fallback_option + return iv_timeseries + + def rt( + self, + expiration: str, + strike: float, + right: str, + dividend_type: Optional[DivType] = DivType.DISCRETE, + american: bool = True, + *, + vol_model: Optional[VolatilityModel] = None, + fallback_option: Optional[RealTimeFallbackOption] = None, + market_model: Optional[OptionPricingModel] = None, + S: Optional[SpotResult] = None, + F: Optional[ForwardResult] = None, + dividends: Optional[DividendsResult] = None, + r: Optional[RatesResult] = None, + market_price: Optional[OptionSpotResult] = None, + undo_adjust: bool = True, + n_steps: Optional[int] = None, + model_price: Optional[ModelPrice] = None, + ) -> VolatilityResult: + """Compute current real-time implied volatility using latest market data. + + Convenience method for real-time volatility calculation. Automatically uses today's + date and QUOTE endpoint source for live market prices. Useful for live trading, + monitoring, and real-time analytics. + + Args: + expiration: Option expiration date (YYYY-MM-DD string or datetime). + strike: Strike price of the option. + right: Option type ('c' for call, 'p' for put). + dividend_type: Dividend treatment (DISCRETE or CONTINUOUS). Defaults to DISCRETE. + american: If True, uses American exercise; if False, European. + market_model: Pricing model to use (BSM, BINOMIAL, EURO_EQIV). Defaults to CONFIG.option_model. + S: Optional pre-computed spot prices. If None, loads automatically. + F: Optional pre-computed forward prices. If None, loads automatically. + dividends: Optional pre-computed dividend data. If None, loads automatically. + r: Optional pre-computed risk-free rates. If None, loads automatically. + market_price: Optional pre-computed option prices. If None, fetches live quotes. + undo_adjust: If True, uses split-adjusted prices. + n_steps: Number of binomial tree steps. Only used for BINOMIAL/EURO_EQIV models. + + Returns: + VolatilityResult with single-row timeseries containing the current implied + volatility. Uses OptionSpotEndpointSource.QUOTE for real-time data. + + Examples: + >>> # Get current IV for a call option + >>> vol_mgr = VolDataManager("AAPL") + >>> rt_vol = vol_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c" + ... ) + >>> print(f"Current IV: {rt_vol.timeseries.iloc[0]:.4f}") + + >>> # Monitor IV throughout the trading day + >>> import time + >>> while market_open: + ... vol = vol_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="p", + ... american=True + ... ) + ... print(f"Current IV: {vol.timeseries.iloc[0]:.4f}") + ... time.sleep(60) # Update every minute + + >>> # Use with specific model + >>> rt_vol = vol_mgr.rt( + ... expiration="2025-06-20", + ... strike=150.0, + ... right="c", + ... market_model=OptionPricingModel.BSM + ... ) + """ + res = self.get_at_time_implied_volatility( + as_of=datetime.now().strftime("%Y-%m-%d"), + expiration=expiration, + strike=strike, + right=right, + dividend_type=dividend_type, + american=american, + market_model=market_model, + S=S, + F=F, + dividends=dividends, + r=r, + market_price=market_price, + undo_adjust=undo_adjust, + n_steps=n_steps, + endpoint_source=OptionSpotEndpointSource.QUOTE, + fallback_option=fallback_option, + model_price=model_price, + vol_model=vol_model, + ) + res.rt = True + return res \ No newline at end of file diff --git a/trade/helpers/Logging.py b/trade/helpers/Logging.py index cfbd35c..f98c598 100644 --- a/trade/helpers/Logging.py +++ b/trade/helpers/Logging.py @@ -5,6 +5,7 @@ from datetime import datetime from zoneinfo import ZoneInfo from dotenv import load_dotenv +from typing import List from logging.handlers import TimedRotatingFileHandler load_dotenv() @@ -23,6 +24,23 @@ def converter(self, timestamp): if self.tz: dt = dt.astimezone(self.tz) return dt.timetuple() + + +def find_logger_names_by_pattern(pattern: str) -> List[str]: + """Find all logger names that start with the given pattern.""" + return [ + name + for name in logging.Logger.manager.loggerDict.keys() + if name.startswith(pattern) + ] + +def find_loggers_by_pattern(pattern: str) -> List[logging.Logger]: + """Find all loggers whose names start with the given pattern.""" + return [ + logging.getLogger(name) + for name in logging.Logger.manager.loggerDict.keys() + if name.startswith(pattern) + ] def find_project_root(current_path: Path, marker=".git"): diff --git a/trade/helpers/decorators.py b/trade/helpers/decorators.py index dcca5b9..08b8363 100644 --- a/trade/helpers/decorators.py +++ b/trade/helpers/decorators.py @@ -1,5 +1,5 @@ # pylint: disable=broad-exception-caught -from trade import register_signal +from trade import register_signal, TIMING_ANALYSIS_CACHE_PATH import time import numbers import asyncio @@ -10,8 +10,6 @@ import pstats import io import traceback -import os -from pathlib import Path from datetime import datetime import signal import pandas as pd @@ -38,7 +36,7 @@ def _save_timeit_metadata(): return try: - cache_path = Path(os.environ.get("GEN_CACHE_PATH", ".cache")) + cache_path = TIMING_ANALYSIS_CACHE_PATH cache_path.mkdir(parents=True, exist_ok=True) # Single CSV file for all timeit logs @@ -412,7 +410,7 @@ def wrapper(*args, **kwargs): stream = io.StringIO() stats = pstats.Stats(profiler, stream=stream).sort_stats("cumulative") stats.print_stats() - return results, stream.getvalue() + return results, stats return wrapper @@ -426,7 +424,7 @@ def cprofiler_func(func, *args, **kwargs): stream = io.StringIO() stats = pstats.Stats(profiler, stream=stream).sort_stats("cumulative") stats.print_stats() - return results, stream.getvalue() + return results, stats def copy_doc(from_func): diff --git a/trade/helpers/exit_helpers.py b/trade/helpers/exit_helpers.py new file mode 100644 index 0000000..ceb9a08 --- /dev/null +++ b/trade/helpers/exit_helpers.py @@ -0,0 +1,45 @@ + +import signal +import pandas as pd +from trade.helpers.Logging import setup_logger +from trade import register_signal, TIMING_ANALYSIS_CACHE_PATH +logger = setup_logger("trade.optionlib.vol.implied_vol") + +TIME_BUCKET = [] + + +def _record_time(start_time: float, end_time: float, action: str, info: dict) -> None: + elapsed = end_time - start_time + meta = { + "action": action, + "elapsed_time": elapsed, + } + meta.update(info) + TIME_BUCKET.append(meta) + + +def _offload_time_bucket(): + """Offload the time bucket to a CSV for analysis.""" + if not TIME_BUCKET: + logger.info("No timing data to offload.") + return + + ## Loc + loc = TIMING_ANALYSIS_CACHE_PATH + file_name = loc / "time_analysis.csv" + loc.mkdir(parents=True, exist_ok=True) + + ## Load old data if exists and append + if file_name.exists(): + old_data = pd.read_csv(file_name) + old_records = old_data.to_dict(orient="records") + TIME_BUCKET.extend(old_records) + + df = pd.DataFrame(TIME_BUCKET) + df.to_csv(file_name, index=False) + TIME_BUCKET.clear() + + +register_signal(signal.SIGTERM, _offload_time_bucket) +register_signal(signal.SIGINT, _offload_time_bucket) +register_signal("exit", _offload_time_bucket) diff --git a/trade/helpers/helper.py b/trade/helpers/helper.py index 76b20f9..5bbd74c 100644 --- a/trade/helpers/helper.py +++ b/trade/helpers/helper.py @@ -1,13 +1,15 @@ ## To-Do: Switch Binomial Pricing to Leisen-Reimer Formulas import inspect import QuantLib as ql -from datetime import datetime +from datetime import datetime, date import time import os import shutil import backoff from dotenv import load_dotenv - +from trade.helpers.helper_types import DATE_HINT, is_iterable +from trade.helpers.vars import SECONDS_IN_DAY, SECONDS_IN_YEAR +from typing import Union, Iterable, TypedDict import sys from enum import Enum from typing import Any, Dict @@ -16,19 +18,17 @@ from pandas.tseries.offsets import BDay from typing import Union from trade.helpers.Configuration import ConfigProxy - +from typing import List import re -from datetime import datetime import QuantLib as ql -from datetime import datetime from dateutil.relativedelta import relativedelta import numpy as np import pandas as pd import yfinance as yf from pandas.tseries.offsets import BDay -from datetime import datetime from trade.helpers.parse import parse_date, parse_time import yfinance as yf +from trade.helpers.vars import register_on_exit from py_vollib.black_scholes import black_scholes as bs from py_vollib.black_scholes.greeks.numerical import delta, vega, theta, rho from py_vollib.black_scholes_merton.implied_volatility import implied_volatility @@ -63,22 +63,27 @@ from trade import get_pool_enabled, register_signal from trade.helpers.pools import runProcesses from trade.helpers.threads import runThreads +from typing import Optional -logger = setup_logger('trade.helpers.helper') +logger = setup_logger("trade.helpers.helper") Configuration = ConfigProxy() load_dotenv() -# To-Dos: +# To-Dos: # If still using binomial, change the r to prompt for it rather than it calling a function option_keys = {} NY = ZoneInfo("America/New_York") + + def ny_now() -> datetime: return datetime.now(tz=NY) + def ny_now_busday() -> datetime: return change_to_last_busday(ny_now()) + def get_parrallel_apply(): """ Get the parallel apply function based on the pool enabled flag. @@ -88,13 +93,14 @@ def get_parrallel_apply(): else: return runThreads -def is_weekend(dt:str|datetime) -> bool: + +def is_weekend(dt: str | datetime) -> bool: """ Check if the given date is a weekend (Saturday or Sunday). - + Args: dt (str | datetime): The date to check. - + Returns: bool: True if the date is a weekend, False otherwise. """ @@ -102,6 +108,23 @@ def is_weekend(dt:str|datetime) -> bool: dt = pd.to_datetime(dt) return dt.weekday() >= 5 # Saturday is 5, Sunday is 6 +def is_market_hours_today() -> bool: + """ + Check if the current time in New York is within market hours (9:30 AM to 4:00 PM) on a business day. + + Returns: + bool: True if within market hours, False otherwise. + """ + now = ny_now() + if now.weekday() >= 5: # Saturday or Sunday + return False + + market_open = now.replace(hour=9, minute=30, second=0, microsecond=0) + market_close = now.replace(hour=16, minute=0, second=0, microsecond=0) + + return market_open <= now <= market_close + + def assert_member_of_enum(value: Any, enum_class: Enum) -> None: """ Assert that the given value is a member of the specified Enum class. @@ -112,7 +135,6 @@ def assert_member_of_enum(value: Any, enum_class: Enum) -> None: return enum_class(value) - def _ipython_shutdown(_callable): """ Register a shutdown function to be called when the IPython kernel is shutting down. @@ -120,10 +142,11 @@ def _ipython_shutdown(_callable): if not callable(_callable): raise TypeError("The shutdown function must be callable.") from IPython import get_ipython + try: ipython = get_ipython() if ipython is not None: - ipython.events.register('shutdown', _callable) + ipython.events.register("shutdown", _callable) except ImportError as e: pass @@ -163,25 +186,27 @@ def _get_val(self, other): return other.value if isinstance(other, Scalar) else other - class CustomCache(Cache): """ CustomCache is a dictionary-like object that stores data on disk. It is a subclass of diskcache.Cache and provides additional functionality """ - def __init__(self, - location: str | Path = None, - fname: str = None, - log_path: str | Path = None, - clear_on_exit: bool = False, - expire_days: int = 7, - data: dict = None, - **kwargs): + + def __init__( + self, + location: str | Path = None, + fname: str = None, + log_path: str | Path = None, + clear_on_exit: bool = False, + expire_days: int = 7, + data: dict = None, + **kwargs, + ): """ Important Behavior: 1. The cache is pegged to a specific on disk data. Represented by location/fname - 2. The cache is cleared on exit if clear_on_exit is set to True. Else, it will remain populated and open. But the location of the directory + 2. The cache is cleared on exit if clear_on_exit is set to True. Else, it will remain populated and open. But the location of the directory will be recorded in a file for later clean-up. - + :params location: str | Path: Folder to store the cache. If None, it will use the WORK_DIR environment variable. :params fname: str: Name of the cache file. Defaults to 'cache'. :params log_path: str | Path: Path to the log file. If None, it will use the WORK_DIR environment variable. @@ -191,41 +216,49 @@ def __init__(self, Example usage: cache = CustomCache(location='/path/to/cache', fname='my_cache', log_path='/path/to/log.txt', clear_on_exit=True) """ - - #1. Check dir & create cache + + # 1. Check dir & create cache fname = str(fname) if fname else shortuuid.random(length=8) - dir = Path(location) / fname if location else Path(os.environ.get('WORK_DIR'))/'.cache'/fname + dir = Path(location) / fname if location else Path(os.environ.get("WORK_DIR")) / ".cache" / fname self.dir = dir self.fname = fname - self.expiry_date = (datetime.today() + relativedelta(days=expire_days)).date().strftime('%Y-%m-%d') + self.expiry_date = (datetime.today() + relativedelta(days=expire_days)).date().strftime("%Y-%m-%d") self._register_location = f'{os.environ["WORK_DIR"]}/trade/helpers/clear_dirs.json' self._owner_pid = os.getpid() # <- track creator - + ## Avoid non path like objects if isinstance(log_path, (str, os.PathLike)): log_path = Path(log_path) elif log_path is None: - log_path = Path(os.environ.get('WORK_DIR'))/'trade'/'helpers'/'cache_clear_log.txt' + log_path = Path(os.environ.get("WORK_DIR")) / "trade" / "helpers" / "cache_clear_log.txt" else: logger.error(f"log_path must be str, Path or None, not {type(log_path)}, recieved {log_path}") - log_path = str(Path(os.environ.get('WORK_DIR'))/'trade'/'helpers'/'cache_clear_log.txt') + log_path = str(Path(os.environ.get("WORK_DIR")) / "trade" / "helpers" / "cache_clear_log.txt") self.__log_path = log_path os.makedirs(dir, exist_ok=True) - - #2. Create cache + + # 2. Create cache super().__init__(dir, **kwargs) - #3. Check if the cache is empty + # 3. Check if the cache is empty self.clear_on_exit = clear_on_exit - - #4. If data is passed, load it into the cache + + # 4. If data is passed, load it into the cache if data is not None: if not isinstance(data, dict): raise ValueError("Data must be a dictionary.") for key, value in data.items(): self[key] = value + @classmethod + def register_to_on_exit(cls, func): + """ + Register a function to be called on exit. + This is useful for when an exit needs to be run BEFORE CustomCache closes connection. + """ + register_on_exit(func) + def __getstate__(self): """ Custom serialization to avoid pickling the cache directory. @@ -236,41 +269,40 @@ def __getstate__(self): log_path=str(self.log_path), clear_on_exit=self.clear_on_exit, expire_days=(pd.to_datetime(self.expiry_date).date() - datetime.today().date()).days, - data=dict(self.items()) + data=dict(self.items()), ) - + def __setstate__(self, state): """ Custom deserialization to restore the cache state. """ self.__init__( - location=state['location'], - fname=state['fname'], - log_path=state['log_path'], - clear_on_exit=state['clear_on_exit'], - expire_days=state['expire_days'], - data=state['data'] + location=state["location"], + fname=state["fname"], + log_path=state["log_path"], + clear_on_exit=state["clear_on_exit"], + expire_days=state["expire_days"], + data=state["data"], ) def __hash__(self): return super().__hash__() - @property def clear_on_exit(self): return self._clear_on_exit - + @clear_on_exit.setter def clear_on_exit(self, value): if not isinstance(value, bool): raise ValueError("clear_on_exit must be a boolean value.") self._clear_on_exit = value self._install_handlers() - + @property def log_path(self): return self.__log_path - + @log_path.setter def log_path(self, value): if not isinstance(value, (str, Path)): @@ -287,26 +319,24 @@ def register_location(self): os.makedirs(os.path.dirname(self._register_location), exist_ok=True) ## Create empty json file - with open(self._register_location, 'w') as f: + with open(self._register_location, "w") as f: json.dump({}, f) return self._register_location - + def _install_handlers(self): """ Central place to register whatever needs doing depending on self._clear_on_exit. """ if self._clear_on_exit: - # atexit.register(self._on_exit) - # signal.signal(signal.SIGTERM, self._on_signal) register_signal(signum=signal.SIGTERM, signal_func=self._on_exit) register_signal(signum=signal.SIGINT, signal_func=self._on_exit) register_signal("exit", self._on_exit) else: # just record the dir for later weekly cron clean-up - with open(self.register_location, 'r') as f: + with open(self.register_location, "r") as f: json_file = json.load(f) - with open(self.register_location, 'w') as f: + with open(self.register_location, "w") as f: loc = str(self.dir) json_file.update({loc: self.expiry_date}) json.dump(json_file, f, default=str) @@ -323,11 +353,10 @@ def items(self): def remove(self, key): if key in self: self.__delitem__(key) - + def pop(self, key, default=None, expire_time=False, tag=False, retry=False): return super().pop(key, default, expire_time, tag, retry) - def update(self, other): if isinstance(other, dict): for key, value in other.items(): @@ -337,7 +366,7 @@ def update(self, other): self[key] = value else: raise ValueError("Other must be a dictionary or CustomCache instance.") - + def filter_keys(self, x): """ Filter the cache keys based on a condition. @@ -347,7 +376,7 @@ def filter_keys(self, x): list: A list of keys that satisfy the condition. """ return [key for key in self.keys() if x(key)] - + def __repr__(self): sample_keys = list(self)[:10] return f"" @@ -355,12 +384,12 @@ def __repr__(self): def __str__(self): sample = dict(list(self.items())[:10]) return f"" - + def setdefault(self, key, default): if key not in self: self[key] = default return self[key] - + def _on_exit(self): try: self.close() @@ -368,13 +397,12 @@ def _on_exit(self): shutil.rmtree(self.dir) except Exception as e: - with open(f'{self.log_path}', 'a') as f: + with open(f"{self.log_path}", "a") as f: f.write(f"Error clearing cache {self.dir} at {datetime.now()}: {e}\n") else: - with open(f'{self.log_path}', 'a') as f: + with open(f"{self.log_path}", "a") as f: f.write(f"Cache {self.dir} cleared by AtExit at {datetime.now()}\n") - def _on_signal(self, signum, frame): # Only the creating process should handle cleanup if os.getpid() != self._owner_pid: @@ -388,7 +416,8 @@ def _on_signal(self, signum, frame): signal.signal(signum, signal.SIG_DFL) except Exception: pass - os.kill(os.getpid(), signum) # default handler runs now; no recursion + os.kill(os.getpid(), signum) # default handler runs now; no recursion + def str_to_bool(value: str) -> bool: """ @@ -398,14 +427,14 @@ def str_to_bool(value: str) -> bool: Returns: bool: True if the string is 'True', '1', or 'yes' (case-insensitive), False otherwise. """ - if value.lower() in ['true', '1', 'yes']: + if value.lower() in ["true", "1", "yes"]: return True - elif value.lower() in ['false', '0', 'no']: + elif value.lower() in ["false", "0", "no"]: return False else: raise ValueError("Invalid boolean string. Expected 'True', 'False', '1', '0', 'yes', or 'no'.") - - + + def check_all_days_available(x, _start, _end): """ Check if all business days in the range are available in the DataFrame x. @@ -413,15 +442,16 @@ def check_all_days_available(x, _start, _end): x (pd.DataFrame): DataFrame with a 'Datetime' column. _start (str or datetime): Start date of the range. _end (str or datetime): End date of the range. - + Returns: bool: True if all business days in the range are available, False otherwise. """ - date_range = bus_range(_start, _end, freq = '1B') + date_range = bus_range(_start, _end, freq="1B") dates_available = x.Datetime missing_dates_second_check = [x for x in date_range if x not in pd.DatetimeIndex(dates_available)] return all(x in pd.DatetimeIndex(dates_available) for x in date_range) + def check_missing_dates(x, _start, _end): """ Check for missing business days in the DataFrame x within the specified date range. This also skips US market holidays. @@ -433,18 +463,19 @@ def check_missing_dates(x, _start, _end): Returns: list: List of missing business days in the range. """ - if 'Datetime' not in x.columns: + if "Datetime" not in x.columns: logger.warning(f"DataFrame does not contain 'Datetime' column. Will default to index") - x['Datetime'] = x.index - date_range = bus_range(_start, _end, freq = '1B') + x["Datetime"] = x.index + date_range = bus_range(_start, _end, freq="1B") dates_available = x.Datetime missing_dates_second_check = [x for x in date_range if x not in pd.DatetimeIndex(dates_available)] missing_dates_third_check = [x for x in missing_dates_second_check if x not in HOLIDAY_SET] missing_dates_fourth_check = [x for x in missing_dates_third_check if x.weekday() < 5] - x.drop(columns=['Datetime'], inplace=True, errors='ignore') + x.drop(columns=["Datetime"], inplace=True, errors="ignore") return missing_dates_fourth_check -def get_missing_dates(x:pd.Series|pd.DataFrame, _start: datetime, _end: datetime): + +def get_missing_dates(x: pd.Series | pd.DataFrame, _start: datetime, _end: datetime) -> List[datetime]: """ Check for missing business days in the Series or DataFrame x within the specified date range. This also skips US market holidays. It also ensures there are no weekends @@ -458,20 +489,20 @@ def get_missing_dates(x:pd.Series|pd.DataFrame, _start: datetime, _end: datetime assert isinstance(x.index, pd.DatetimeIndex), "DataFrame index must be a DatetimeIndex" date_range = bus_range(_start, _end, freq="1B") dates_available = x.index - + # Numpy optimized version - O(n log n) vs O(n²) - date_range_arr = np.array(date_range, dtype='datetime64[ns]') - dates_available_arr = np.array(dates_available, dtype='datetime64[ns]') - + date_range_arr = np.array(date_range, dtype="datetime64[ns]") + dates_available_arr = np.array(dates_available, dtype="datetime64[ns]") + # Check which dates are missing from available dates missing_mask = ~np.isin(date_range_arr, dates_available_arr) missing_dates_arr = date_range_arr[missing_mask] - + # Filter out holidays using numpy - holiday_arr = np.array(list(HOLIDAY_SET), dtype='datetime64[ns]') + holiday_arr = np.array(list(HOLIDAY_SET), dtype="datetime64[ns]") not_holiday_mask = ~np.isin(missing_dates_arr, holiday_arr) missing_dates_no_holidays_arr = missing_dates_arr[not_holiday_mask] - + # Filter out weekends using vectorized weekday check if len(missing_dates_no_holidays_arr) > 0: missing_dates_idx = pd.DatetimeIndex(missing_dates_no_holidays_arr) @@ -479,9 +510,10 @@ def get_missing_dates(x:pd.Series|pd.DataFrame, _start: datetime, _end: datetime missing_dates_fourth_check = missing_dates_idx[weekdays < 5] else: missing_dates_fourth_check = pd.DatetimeIndex(missing_dates_no_holidays_arr) - + return missing_dates_fourth_check.tolist() + # def get_missing_dates(x:pd.Series|pd.DataFrame, _start: datetime, _end: datetime): # """ # Check for missing business days in the Series or DataFrame x within the specified date range. This also skips US market holidays. @@ -501,16 +533,19 @@ def get_missing_dates(x:pd.Series|pd.DataFrame, _start: datetime, _end: datetime # missing_dates_fourth_check = [x for x in missing_dates_third_check if x.weekday() < 5] # return missing_dates_fourth_check -def vol_backout_errors(sigma, K, S0, T, r, q, market_price, flag): +def vol_backout_errors(sigma, K, S0, T, r, q, market_price, flag): """Check for errors in the input parameters for the vol backout function""" import numbers + assert isinstance(sigma, numbers.Number), f"Recieved '{type(sigma)}' for sigma. Expected 'int' or 'float'" assert isinstance(K, numbers.Number), f"Recieved '{type(K)}' for K. Expected 'int' or 'float'" assert isinstance(S0, numbers.Number), f"Recieved '{type(S0)}' for S0. Expected 'int' or 'float'" assert isinstance(r, numbers.Number), f"Recieved '{type(r)}' for r. Expected 'int' or 'float'" assert isinstance(q, numbers.Number), f"Recieved '{type(q)}' for q. Expected 'int' or 'float'" - assert isinstance(market_price, numbers.Number), f"Recieved '{type(market_price)}' for market_price. Expected 'int' or 'float'" + assert isinstance( + market_price, numbers.Number + ), f"Recieved '{type(market_price)}' for market_price. Expected 'int' or 'float'" assert isinstance(flag, str), f"Recieved '{type(flag)}' for flag. Expected 'str'" if sigma <= 0: @@ -527,39 +562,43 @@ def vol_backout_errors(sigma, K, S0, T, r, q, market_price, flag): raise ValueError("Dividend yield must be non-negative.") if market_price <= 0: raise ValueError("Market price must be positive.") - if flag not in ['c', 'p']: + if flag not in ["c", "p"]: raise ValueError("Flag must be 'c' for call or 'p' for put.") - + if pd.isna(sigma) or pd.isna(K) or pd.isna(S0) or pd.isna(r) or pd.isna(q) or pd.isna(market_price): raise ValueError("Input values cannot be NaN.") -def save_vol_resolve(opt_tick, datetime, vol_resolve, agg = 'eod'): + +def save_vol_resolve(opt_tick, datetime, vol_resolve, agg="eod"): """Utility function to save vol_resolve to json file""" import os, json - with open(f'{os.environ["WORK_DIR"]}/trade/helpers/vol_resolve_{agg}.json', 'r') as f: + + with open(f'{os.environ["WORK_DIR"]}/trade/helpers/vol_resolve_{agg}.json', "r") as f: data = json.load(f) - datetime = pd.to_datetime(datetime).strftime('%Y-%m-%d') + datetime = pd.to_datetime(datetime).strftime("%Y-%m-%d") data.setdefault(datetime, {}) - data[datetime][opt_tick]= {} - data[datetime][opt_tick]['VolResolve'] = vol_resolve - with open(f'{os.environ["WORK_DIR"]}/trade/helpers/vol_resolve_{agg}.json', 'w') as f: + data[datetime][opt_tick] = {} + data[datetime][opt_tick]["VolResolve"] = vol_resolve + with open(f'{os.environ["WORK_DIR"]}/trade/helpers/vol_resolve_{agg}.json', "w") as f: json.dump(data, f) def import_option_keys(): global option_keys import json - with open(f'{os.environ["WORK_DIR"]}/trade/assets/option_key.json', 'rb') as f: + + with open(f'{os.environ["WORK_DIR"]}/trade/assets/option_key.json', "rb") as f: option_keys = json.load(f) def save_option_keys(key, info): import json + global option_keys import_option_keys() if key not in option_keys.keys(): - option_keys[key] = info - with open(f'{os.environ["WORK_DIR"]}/trade/assets/option_key.json', 'w') as f: + option_keys[key] = info + with open(f'{os.environ["WORK_DIR"]}/trade/assets/option_key.json', "w") as f: json.dump(option_keys, f) @@ -579,21 +618,16 @@ def filter_inf(data): data = data.replace([np.inf, -np.inf], np.nan) return data.ffill() + def filter_zeros(data): data = data.replace(0, np.nan) return data.ffill() -@backoff.on_exception(backoff.expo, - (OpenBBEmptyData, YFinanceEmptyData), - max_tries=5, - logger=logger) -def retrieve_timeseries(tick, - start, - end, - interval = '1d', - provider = 'yfinance', - spot_type='close', - **kwargs) -> pd.DataFrame: + +@backoff.on_exception(backoff.expo, (OpenBBEmptyData, YFinanceEmptyData), max_tries=5, logger=logger) +def retrieve_timeseries( + tick, start, end, interval="1d", provider="yfinance", spot_type="close", **kwargs +) -> pd.DataFrame: """ Returns an OHLCV for provided ticker. @@ -608,39 +642,47 @@ def retrieve_timeseries(tick, Returns: pd.DataFrame: DataFrame with OHLCV data and additional columns for split adjustments """ - if spot_type == 'chain_price': - df = retrieve_timeseries(tick, end =(change_to_last_busday(datetime.today())+ BDay(1)).strftime('%Y-%m-%d'), - start = '1960-01-01', interval= interval, provider = provider) + if spot_type == "chain_price": + df = retrieve_timeseries( + tick, + end=(change_to_last_busday(datetime.today()) + BDay(1)).strftime("%Y-%m-%d"), + start="1960-01-01", + interval=interval, + provider=provider, + ) df.index = pd.to_datetime(df.index) df = df[(df.index >= pd.Timestamp(start)) & (df.index <= pd.Timestamp(end))] - df['close'] = df['chain_price'] - df['cum_split_from_start'] = df['split_ratio'].cumprod() + df["close"] = df["chain_price"] + df["cum_split_from_start"] = df["split_ratio"].cumprod() return df else: try: - ## yfinance needs end date to be + 1 day to be inclusive. Doing this before the function call because it's undone later end = pd.to_datetime(end) + relativedelta(days=1) + def query_data(start, end, tick, interval): - data = yf.download(tick, start=start, end = end, interval=interval, multi_level_index=False, progress=False, actions = True) - data.rename(columns={'Stock Splits': 'split_ratio', 'Dividends': 'dividends'}, inplace=True) - data = data.loc[:, ~data.columns.duplicated()] ## For some reason columns are duplicated sometimes - data.columns = data.columns.str.lower() + data = yf.download( + tick, start=start, end=end, interval=interval, multi_level_index=False, progress=False, actions=True + ) + data.rename(columns={"Stock Splits": "split_ratio", "Dividends": "dividends"}, inplace=True) + data = data.loc[:, ~data.columns.duplicated()] ## For some reason columns are duplicated sometimes + data.columns = data.columns.str.lower() return data - + data = query_data(start=start, end=end, tick=tick, interval=interval) ## Check if data is empty. This raises YFinanceEmptyData for backoff to catch if data.empty: raise YFinanceEmptyData(f"OpenBB returned empty data for {tick} with {provider} provider") - - ## Retry logic for missing split_ratio column - if 'split_ratio' not in data.columns: + ## Retry logic for missing split_ratio column + if "split_ratio" not in data.columns: ## `close` spot type means split_ratio isn't important. Set to 1 - if spot_type == 'close': - data['split_ratio'] = 1 - logger.info(f"No splits found for {tick} between {start} and {end}. Added split_ratio column with 1s") + if spot_type == "close": + data["split_ratio"] = 1 + logger.info( + f"No splits found for {tick} between {start} and {end}. Added split_ratio column with 1s" + ) ## `chain_price` spot type means split_ratio is important. Retry up to 3 times else: @@ -651,29 +693,33 @@ def query_data(start, end, tick, interval): data = query_data(start=start, end=end, tick=tick, interval=interval) ## If found, break - if 'split_ratio' in data.columns: + if "split_ratio" in data.columns: break - + ## Else, wait 2 seconds and retry time.sleep(2) retry_counter += 1 - + ## Final check: if still not found, raise error - if 'split_ratio' not in data.columns: - raise YFinanceEmptyData(f"yfinance returned data without split_ratio column for {tick} after 3 retries") - + if "split_ratio" not in data.columns: + raise YFinanceEmptyData( + f"yfinance returned data without split_ratio column for {tick} after 3 retries" + ) + ## Filter Data within range - data = data[(data.index.date >= pd.to_datetime(start).date()) & - (data.index.date <= (pd.to_datetime(end) - relativedelta(days=1)).date())] - except Exception as e: ## Unnecessary placeholder, I know. Will look for best idea for this. + data = data[ + (data.index.date >= pd.to_datetime(start).date()) + & (data.index.date <= (pd.to_datetime(end) - relativedelta(days=1)).date()) + ] + except Exception as e: ## Unnecessary placeholder, I know. Will look for best idea for this. raise e - data['split_ratio'].replace(0, 1, inplace = True) - data['cum_split'] = data['split_ratio'].cumprod() - data['max_cum_split'] = data.cum_split.max() - data['unadjusted_close'] = data.close * data.max_cum_split - data['split_factor'] = data.max_cum_split / data.cum_split - data['chain_price'] = data.close * data.split_factor + data["split_ratio"].replace(0, 1, inplace=True) + data["cum_split"] = data["split_ratio"].cumprod() + data["max_cum_split"] = data.cum_split.max() + data["unadjusted_close"] = data.close * data.max_cum_split + data["split_factor"] = data.max_cum_split / data.cum_split + data["chain_price"] = data.close * data.split_factor data = data[ [ "open", @@ -689,63 +735,72 @@ def query_data(start, end, tick, interval): "max_cum_split", ] ] - data['is_split_date'] = data['split_ratio'] != 1 + data["is_split_date"] = data["split_ratio"] != 1 data.index = pd.to_datetime(data.index) - ## To-Do: Add a data cleaning function to remove zeros and inf and check for other anomalies. + ## To-Do: Add a data cleaning function to remove zeros and inf and check for other anomalies. ## In the function, add a logger to log the anomalies - if data.empty and provider == 'yfinance': + if data.empty and provider == "yfinance": logger.warning(f"yfinance returned empty data for {tick} is empty") raise YFinanceEmptyData(f"yfinance returned empty data for {tick} is empty") ## Fix intraday data missing 16:00:00 timestamp - if 'h' in interval or 'm' in interval: - if 'm' in interval: + if "h" in interval or "m" in interval: + if "m" in interval: ## Pandas doesn't like the 'm' in the interval, so we need to replace it with 'min'. 'm' is month in pandas - interval = interval.replace('m', 'min') + interval = interval.replace("m", "min") data = enforce_bus_hours(data) reindex = bus_range(data.index[0], data.index[-1], interval) - data = data.reindex(reindex, method='ffill').dropna() + data = data.reindex(reindex, method="ffill").dropna() - return data -def identify_interval(timewidth, timeframe, provider = 'default'): - if provider == 'yfinance': - TIMEFRAMES = {'day': 'd', 'hour': 'h', 'minute': 'm', 'week': 'W', 'month': 'M', 'quarter': 'Q'} - assert timeframe.lower() in TIMEFRAMES.keys(), f"For '{provider}' provider timeframes, these are your options, {TIMEFRAMES.keys()}" + +def identify_interval(timewidth, timeframe, provider="default"): + if provider == "yfinance": + TIMEFRAMES = {"day": "d", "hour": "h", "minute": "m", "week": "W", "month": "M", "quarter": "Q"} + assert ( + timeframe.lower() in TIMEFRAMES.keys() + ), f"For '{provider}' provider timeframes, these are your options, {TIMEFRAMES.keys()}" return f"{str(timewidth)}{TIMEFRAMES[timeframe.lower()]}" - - elif provider == 'default': - TIMEFRAMES = {'day': 'd', 'hour': 'h', 'minute': 'm', 'week': 'w', 'month': 'M', 'quarter': 'q', 'year': 'y'} - assert timeframe.lower() in TIMEFRAMES.keys(), f"For '{provider}' provider timeframes, these are your options, {TIMEFRAMES.keys()}" + + elif provider == "default": + TIMEFRAMES = {"day": "d", "hour": "h", "minute": "m", "week": "w", "month": "M", "quarter": "q", "year": "y"} + assert ( + timeframe.lower() in TIMEFRAMES.keys() + ), f"For '{provider}' provider timeframes, these are your options, {TIMEFRAMES.keys()}" return f"{str(timewidth)}{TIMEFRAMES[timeframe.lower()]}" - - - elif provider == 'fmp': - TIMEFRAMES = {'day': 'd', 'hour': 'h', 'minute': 'm'} - assert timeframe.lower() in TIMEFRAMES.keys(), f"For '{provider}' provider timeframes, these are your options, {TIMEFRAMES.keys()}" + + elif provider == "fmp": + TIMEFRAMES = {"day": "d", "hour": "h", "minute": "m"} + assert ( + timeframe.lower() in TIMEFRAMES.keys() + ), f"For '{provider}' provider timeframes, these are your options, {TIMEFRAMES.keys()}" return f"{str(timewidth)}{TIMEFRAMES[timeframe.lower()]}" - def identify_length(string, integer): - TIMEFRAMES_VALUES = {'d': 1, 'w': 5, 'm': 30, 'y': 252, 'q': 91} - assert string in TIMEFRAMES_VALUES.keys(), f'Available timeframes are {TIMEFRAMES_VALUES.keys()}, recieved "{string}"' + TIMEFRAMES_VALUES = {"d": 1, "w": 5, "m": 30, "y": 252, "q": 91} + assert ( + string in TIMEFRAMES_VALUES.keys() + ), f'Available timeframes are {TIMEFRAMES_VALUES.keys()}, recieved "{string}"' return integer * TIMEFRAMES_VALUES[string] + def extract_numeric_value(timeframe_str): - match = re.findall(r'(\d+)([a-zA-Z]+)', timeframe_str) + match = re.findall(r"(\d+)([a-zA-Z]+)", timeframe_str) integers = [int(num) for num, _ in match][0] strings = [str(letter) for _, letter in match][0] return strings, integers + def enforce_allowed_models(model: list) -> list: """ Ensures that the model is in the allowed models list. """ - assert model in PRICING_CONFIG['AVAILABLE_PRICING_MODELS'], f"Model {model} is not in the allowed models list. Expected {PRICING_CONFIG['AVAILABLE_PRICING_MODELS']}" - + assert ( + model in PRICING_CONFIG["AVAILABLE_PRICING_MODELS"] + ), f"Model {model} is not in the allowed models list. Expected {PRICING_CONFIG['AVAILABLE_PRICING_MODELS']}" def date_inbetween(date, start, end, inclusive=True): @@ -764,34 +819,36 @@ def date_inbetween(date, start, end, inclusive=True): else: return start < date < end + class compare_dates: """ A class to compare dates with various methods. """ + @staticmethod def is_before(date1, date2): - """ Check if date1 is before date2.""" + """Check if date1 is before date2.""" return pd.to_datetime(date1) < pd.to_datetime(date2) @staticmethod def is_after(date1, date2): - """ Check if date1 is after date2.""" + """Check if date1 is after date2.""" return pd.to_datetime(date1) > pd.to_datetime(date2) - + @staticmethod def is_on_or_before(date1, date2): - """ Check if date1 is on or before date2.""" + """Check if date1 is on or before date2.""" return pd.to_datetime(date1) <= pd.to_datetime(date2) - + @staticmethod def is_on_or_after(date1, date2): - """ Check if date1 is on or after date2.""" + """Check if date1 is on or after date2.""" return pd.to_datetime(date1) >= pd.to_datetime(date2) @staticmethod def is_equal(date1, date2): return pd.to_datetime(date1) == pd.to_datetime(date2) - + @staticmethod def inbetween(date, start, end, inclusive=True): """ @@ -806,30 +863,30 @@ def inbetween(date, start, end, inclusive=True): return date_inbetween(date, start, end, inclusive) - -def print_cprofile_internal_time_share(_stats, top_n=20, sort_by='tottime', full_name=False): +def print_cprofile_internal_time_share(_stats, top_n=20, sort_by="tottime", full_name=False): """ Print top n functions by internal (self) time, with their share of total self time. """ _stats = deepcopy(_stats) _stats.sort_stats(sort_by) - + all_stats = _stats.stats.items() - total_self_time = sum(stat[2] for _, stat in all_stats) + total_self_time = sum(stat[2] for _, stat in all_stats) top_list = sorted(all_stats, key=lambda x: x[1][2], reverse=True)[:top_n] - print(f"{'Function':<70} {'SelfTime':>10} {'ShareOfTotal':>12}") - print('-' * 95) + print(f"{'Function':<100} {'SelfTime':>10} {'ShareOfTotal':>12}") + print("-" * 115) for func, stat in top_list: filename, line, funcname = func label = f"{filename}:{line} {funcname}" if full_name else funcname self_time = stat[2] ratio = self_time / total_self_time if total_self_time else 0 - print(f"{label:<70} {self_time:>10.4f} {ratio:>12.2%}") + print(f"{label:<100} {self_time:>10.4f} {ratio:>12.2%}") -def print_top_cprofile_stats(_stats, top_n=20, sort_by='cumulative', full_name=False): + +def print_top_cprofile_stats(_stats, top_n=20, sort_by="cumulative", full_name=False): """ Display the top n functions from a cProfile stats file, showing cumulative time and ratio to the top function. @@ -847,8 +904,8 @@ def print_top_cprofile_stats(_stats, top_n=20, sort_by='cumulative', full_name=F top_cum_time = top_list[0][1][3] # Header - print(f"{'Function':<80} {'CumTime':>10} {'RatioToTop':>12}") - print('-' * 105) + print(f"{'Function':<125} {'CumTime':>10} {'RatioToTop':>12}") + print("-" * 150) for func, stat in top_list: filename, line, funcname = func @@ -860,12 +917,14 @@ def print_top_cprofile_stats(_stats, top_n=20, sort_by='cumulative', full_name=F else: label = funcname - print(f"{label:<80} {cum_time:>10.4f} {ratio:>12.2f}") + # Truncate label to fit column width + if len(label) > 122: + label = label[:122] + "..." + + print(f"{label:<125} {cum_time:>10.4f} {ratio:>12.2f}") -def find_split_dates_within_range(tick: str, - start: str, - end: str): +def find_split_dates_within_range(tick: str, start: str, end: str): """ Find split dates within a range params: @@ -875,54 +934,91 @@ def find_split_dates_within_range(tick: str, return: list of split dates within the range - - + + """ - data = retrieve_timeseries(tick, '1900-01-01', end, '1d') + data = retrieve_timeseries(tick, "1900-01-01", end, "1d") data = data[data.index.date >= pd.to_datetime(start).date()] - return list(data[data['is_split_date'] == True]['split_ratio'].to_frame().itertuples(name = None)) - + return list(data[data["is_split_date"] == True]["split_ratio"].to_frame().itertuples(name=None)) def printmd(string): from IPython.display import Markdown, display + display(Markdown(string)) + def copy_doc_from(func): def wrapper(method): method.__doc__ = func.__doc__ return method - return wrapper + return wrapper def contains_time_format(date_str: str) -> bool: try: - datetime.strptime(date_str, '%H:%M:%S') + datetime.strptime(date_str, "%H:%M:%S") return True except ValueError: return False -def time_distance_helper(exp: str, strt: str = None) -> float: + +def assert_equal_length(*args, names: list = None): """ - Calculate the time distance between two dates in years. + Assert that all input lists have the same length. + """ + lengths = [len(arg) for arg in args] + if len(set(lengths)) != 1: + if names is not None: + name_length_pairs = ", ".join(f"{name}: {length}" for name, length in zip(names, lengths)) + raise ValueError(f"Input lists must have the same length. Lengths are: {name_length_pairs}") + else: + raise ValueError(f"Input lists must have the same length. Lengths are: {lengths}") + return True + + +def time_distance_helper(start: Union[DATE_HINT, Iterable[DATE_HINT]], + end: Union[DATE_HINT, Iterable[DATE_HINT]]) -> Union[float, np.ndarray]: + """Calculates time distance in years between two dates.""" + initial_is_iterable = is_iterable(start, include_str=False) or is_iterable(end, include_str=False) + ## Ensure iterable + if not is_iterable(start, include_str=False): + start = [start] + if not is_iterable(end, include_str=False): + end = [end] + + ## Assert equal length + assert_equal_length(start, end, names=("start", "end")) + + ## Convert to datetime + start = np.array(start, dtype='datetime64[D]') + end = np.array(end, dtype='datetime64[D]') + + ## Calculate time distance in years + dte = (end - start)/np.timedelta64(1, 'D') + dte_in_seconds = dte * SECONDS_IN_DAY + dte_in_years = dte_in_seconds / SECONDS_IN_YEAR + + + if not initial_is_iterable: + return dte_in_years[0] + return dte_in_years + + +def binomial( + K: Union[int, float], + exp_date: str, + sigma: float, + r: float = None, + N: int = 100, + S0: Union[int, float, None] = None, + y: float = None, + tick: str = None, + opttype="P", + start: str = None, +) -> float: """ - if strt is None: - strt = datetime.today() - - exp = pd.to_datetime(exp) - exp = exp.replace(hour = 16, minute = 0, second = 0, microsecond = 0,) - parsed_dte, start_date = pd.to_datetime(exp), pd.to_datetime(strt) - if start_date.hour == 0 and start_date.minute == 0 and start_date.second == 0: - start_date = start_date.replace(hour=16, minute=0, second=0, microsecond=0) - days = (parsed_dte - start_date).total_seconds() - - T = days/(365.25*24*3600) - return T - - -def binomial(K: Union[int, float], exp_date: str, sigma: float, r: float = None, N: int = 100, S0: Union[int, float, None] = None, y: float = None, tick: str = None, opttype='P', start: str = None) -> float: - ''' Returns the price of an american option Parameters: @@ -935,7 +1031,7 @@ def binomial(K: Union[int, float], exp_date: str, sigma: float, r: float = None, Sigma: Implied Volatility of the option opttype: Option type ie put or call (Defaults to "P") start: Start date of the pricing model. If nothing is passed, defaults to today. If initiated within a context and nothing is passed, defaults to context start date (Optional) - ''' + """ if start is None: if Configuration.start_date is not None: start = Configuration.start_date @@ -954,37 +1050,37 @@ def binomial(K: Union[int, float], exp_date: str, sigma: float, r: float = None, y = 0 if r is None: rates = 0.005 - r = rates.iloc[len(rates)-1, 0]/100 + r = rates.iloc[len(rates) - 1, 0] / 100 # Create a formula to get implied vol - T = time_distance_helper(exp_date, start) - dt = T/N - nu = r - 0.5*sigma**2 - u = np.exp(nu*dt + sigma*np.sqrt(dt)) - d = np.exp(nu*dt - sigma*np.sqrt(dt)) - q = (np.exp((r-y)*dt) - d) / (u-d) - disc = np.exp(-(r-y)*dt) + T = time_distance_helper(end=exp_date, start=start) + dt = T / N + nu = r - 0.5 * sigma**2 + u = np.exp(nu * dt + sigma * np.sqrt(dt)) + d = np.exp(nu * dt - sigma * np.sqrt(dt)) + q = (np.exp((r - y) * dt) - d) / (u - d) + disc = np.exp(-(r - y) * dt) opttype = opttype.upper() # initialise stock prices at maturity (calculating final stock values at the last nodes) - S = np.zeros(N+1) - for j in range(0, N+1): - S[j] = S0 * u**j * d**(N-j) + S = np.zeros(N + 1) + for j in range(0, N + 1): + S[j] = S0 * u**j * d ** (N - j) # option payoff, (calculating the payoffs at each final node.) - C = np.zeros(N+1) - for j in range(0, N+1): - if opttype == 'P': + C = np.zeros(N + 1) + for j in range(0, N + 1): + if opttype == "P": C[j] = max(0, K - S[j]) else: C[j] = max(0, S[j] - K) # backward recursion through the tree - for i in np.arange(N-1, -1, -1): - for j in range(0, i+1): - S = S0 * u**j * d**(i-j) - C[j] = disc * (q*C[j+1] + (1-q)*C[j]) - if opttype == 'P': + for i in np.arange(N - 1, -1, -1): + for j in range(0, i + 1): + S = S0 * u**j * d ** (i - j) + C[j] = disc * (q * C[j + 1] + (1 - q) * C[j]) + if opttype == "P": C[j] = max(C[j], K - S) else: C[j] = max(C[j], S - K) @@ -992,25 +1088,25 @@ def binomial(K: Union[int, float], exp_date: str, sigma: float, r: float = None, return C[0] -def implied_vol_bs_helper(S0, K, T, r, market_price, flag='c', tol=1e-3, exp_date='2024-03-08'): +def implied_vol_bs_helper(S0, K, T, r, market_price, flag="c", tol=1e-3, exp_date="2024-03-08"): """Compute the implied volatility of a European Option - S0: initial stock price - K: strike price - T: maturity - r: risk-free rate - market_price: market observed price - tol: user choosen tolerance + S0: initial stock price + K: strike price + T: maturity + r: risk-free rate + market_price: market observed price + tol: user choosen tolerance """ max_iter = 200 # max number of iterations vol_old = 0.5 # initial guess count = 0 for k in range(max_iter): bs_price = bs(flag, S0, K, T, r, vol_old) - Cprime = vega(flag, S0, K, T, r, vol_old)*100 + Cprime = vega(flag, S0, K, T, r, vol_old) * 100 C = bs_price - market_price - vol_new = vol_old - C/Cprime + vol_new = vol_old - C / Cprime bs_new = bs(flag, S0, K, T, r, vol_new) - if (abs((vol_old - vol_new)/vol_old)) < tol: + if (abs((vol_old - vol_new) / vol_old)) < tol: break vol_old = vol_new implied_vol = vol_old @@ -1018,19 +1114,23 @@ def implied_vol_bs_helper(S0, K, T, r, market_price, flag='c', tol=1e-3, exp_dat return implied_vol -def implied_vol_bt(S0, K, r, market_price,exp_date: str, flag='c', tol=0.000000000001, y=None, start = None, break_time = 60): +def implied_vol_bt( + S0, K, r, market_price, exp_date: str, flag="c", tol=0.000000000001, y=None, start=None, break_time=60 +): """Compute the implied volatility of an American Option - S0: initial stock price - K: strike price - r: risk-free rate - y: Dividend yield - market_price: market observed price - tol: user choosen tolerance + S0: initial stock price + K: strike price + r: risk-free rate + y: Dividend yield + market_price: market observed price + tol: user choosen tolerance """ if pd.to_datetime(exp_date) == pd.to_datetime(start): - logger.warning(f"Expiration date {exp_date} is the same as start date {start}. Include HH:MM:SS in the start date, to prevent pricing EOD") + logger.warning( + f"Expiration date {exp_date} is the same as start date {start}. Include HH:MM:SS in the start date, to prevent pricing EOD" + ) - T = time_distance_helper(exp_date, start) + T = time_distance_helper(end=exp_date, start=start) max_iter = 200 # max number of iterations vol_old = 0.2 # initial guess count = 0 @@ -1039,28 +1139,31 @@ def implied_vol_bt(S0, K, r, market_price,exp_date: str, flag='c', tol=0.0000000 for k in range(max_iter): current_time = time.time() if current_time - start_time > break_time: - logger.error(f"Binomial Implied vol took too long to calculate for {S0}, {K}, {r}, {market_price}, {exp_date}, {flag}, total time: {current_time - start_time}") + logger.error( + f"Binomial Implied vol took too long to calculate for {S0}, {K}, {r}, {market_price}, {exp_date}, {flag}, total time: {current_time - start_time}" + ) return 0.0 - bs_price = binomial( - K=K, exp_date=exp_date, S0=S0, r=r, sigma=vol_old, opttype=flag, y=y, start = start) + bs_price = binomial(K=K, exp_date=exp_date, S0=S0, r=r, sigma=vol_old, opttype=flag, y=y, start=start) - Cprime = vega(flag, S0, K, T, r, vol_old)*100 + Cprime = vega(flag, S0, K, T, r, vol_old) * 100 C = bs_price - market_price - vol_new = vol_old - C/Cprime + vol_new = vol_old - C / Cprime vol_new = np.clip(vol_new, 0.0001, 5) - if (abs((vol_old - vol_new)/vol_old)) < tol: + if (abs((vol_old - vol_new) / vol_old)) < tol: break vol_old = vol_new count += 1 implied_vol = vol_old if pd.isna(implied_vol) or implied_vol == 0.0: - logger.warning(f"Binomial Implied vol is NaN for {S0}, {K}, {r}, {market_price}, Exp: {exp_date}, Flag: {flag}, Start: {start}") + logger.warning( + f"Binomial Implied vol is NaN for {S0}, {K}, {r}, {market_price}, Exp: {exp_date}, Flag: {flag}, Start: {start}" + ) return 0.0 return implied_vol def d1_helper(S, K, r, T, sigma, q): - return (np.log(S / K) + ((r-q) + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T)) + return (np.log(S / K) + ((r - q) + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T)) def d2_helper(S, K, r, T, sigma, q): @@ -1073,11 +1176,11 @@ def volga(S, K, r, T, sigma, flag, q): flag = flag.upper() if sigma <= 0: raise ValueError("Volatility must be positive.") - if flag == 'C' or flag == 'P': - if flag.upper() == 'C': - volga = (d1*d2*S*np.exp(-q*T)*norm.cdf(d1)*np.sqrt(T))/sigma + if flag == "C" or flag == "P": + if flag.upper() == "C": + volga = (d1 * d2 * S * np.exp(-q * T) * norm.cdf(d1) * np.sqrt(T)) / sigma else: - volga = (d1*d2*S*np.exp(-q*T)*norm.cdf(-d1)*np.sqrt(T))/sigma + volga = (d1 * d2 * S * np.exp(-q * T) * norm.cdf(-d1) * np.sqrt(T)) / sigma else: raise ValueError("Invalid Option Type. Only 'C' for Call and 'P' for Put are available.") return volga @@ -1090,11 +1193,11 @@ def vanna(S, K, r, T, sigma, flag, q): if sigma <= 0: raise ValueError("Volatility must be positive.") flag = flag.upper() - if flag == 'C' or flag == 'P': - if flag.upper() == 'C': - vanna = -(d2 * np.exp(-q*T)*norm.cdf(d1))/sigma + if flag == "C" or flag == "P": + if flag.upper() == "C": + vanna = -(d2 * np.exp(-q * T) * norm.cdf(d1)) / sigma else: - vanna = -(d2 * np.exp(-q*T)*norm.cdf(-d1))/sigma + vanna = -(d2 * np.exp(-q * T) * norm.cdf(-d1)) / sigma else: raise ValueError("Invalid Option Type. Only 'C' for Call and 'P' for Put are available.") return vanna @@ -1103,39 +1206,46 @@ def vanna(S, K, r, T, sigma, flag, q): def phi(x): return norm.pdf(x) + def N(x): return norm.cdf(x) -def d1(S,K,r,T,sigma,q): - return (np.log(S/K) + (r - q + 0.5*sigma**2)*T) / (sigma*np.sqrt(T)) -def d2(S,K,r,T,sigma,q): - return d1(S,K,r,T,sigma,q) - sigma*np.sqrt(T) +def d1(S, K, r, T, sigma, q): + return (np.log(S / K) + (r - q + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T)) + + +def d2(S, K, r, T, sigma, q): + return d1(S, K, r, T, sigma, q) - sigma * np.sqrt(T) + -def vega_decimal(S,K,r,T,sigma,q): - return S*np.exp(-q*T)*phi(d1(S,K,r,T,sigma,q))*np.sqrt(T) +def vega_decimal(S, K, r, T, sigma, q): + return S * np.exp(-q * T) * phi(d1(S, K, r, T, sigma, q)) * np.sqrt(T) -def volga_decimal(S,K,r,T,sigma,q): - d_1 = d1(S,K,r,T,sigma,q); d_2 = d2(S,K,r,T,sigma,q) - return vega_decimal(S,K,r,T,sigma,q) * d_1 * d_2 / sigma -def vanna_decimal(S,K,r,T,sigma,q): - d_1 = d1(S,K,r,T,sigma,q); d_2 = d2(S,K,r,T,sigma,q) - return np.exp(-q*T)*phi(d_1) * (-d_2) / sigma +def volga_decimal(S, K, r, T, sigma, q): + d_1 = d1(S, K, r, T, sigma, q) + d_2 = d2(S, K, r, T, sigma, q) + return vega_decimal(S, K, r, T, sigma, q) * d_1 * d_2 / sigma + + +def vanna_decimal(S, K, r, T, sigma, q): + d_1 = d1(S, K, r, T, sigma, q) + d_2 = d2(S, K, r, T, sigma, q) + return np.exp(-q * T) * phi(d_1) * (-d_2) / sigma def optionPV_helper( spot_price: float, strike_price: float | int, - exp_date: str | datetime, + exp_date: str | datetime, risk_free_rate: float, dividend_yield: float, volatility: float, putcall: str, settlement_date_str: str, - model: str = 'bs' + model: str = "bs", ): - """ Price an American option using QuantLib Engine @@ -1148,13 +1258,13 @@ def optionPV_helper( risk_free_rate: Prevailing discount rate, annualized and expressed as 0.01 for 1% volatility: Underlying Volatility settlement_date_str: pricing date - model: Preferred pricing method. + model: Preferred pricing method. Available options: 'bsm': Black Scholes Model 'bt': Binomial Tree Model 'mcs': Monte Carlo Simulation - Returns: + Returns: ____________ PV (float): Option present value @@ -1164,45 +1274,48 @@ def optionPV_helper( try: # Option Parameters - - if model == 'binomial': + if model == "binomial": binomial_price = binomial( - K = strike_price, - exp_date = exp_date, - sigma = volatility, - r = risk_free_rate, + K=strike_price, + exp_date=exp_date, + sigma=volatility, + r=risk_free_rate, S0=spot_price, - y = dividend_yield, + y=dividend_yield, opttype=putcall, - start = settlement_date_str + start=settlement_date_str, ) return binomial_price - spot_price = spot_price # Current stock price + spot_price = spot_price # Current stock price strike_price = strike_price # Option strike price maturity_date_str = exp_date # Option maturity date as a string risk_free_rate = risk_free_rate # Risk-free interest rate volatility = volatility # Volatility of the underlying asset - dividend_yield = dividend_yield # Continuous dividend yield + dividend_yield = dividend_yield # Continuous dividend yield # Convert string date to QuantLib Date - maturity_date = ql.Date(pd.to_datetime(maturity_date_str).day, - pd.to_datetime(maturity_date_str).month, - pd.to_datetime(maturity_date_str).year) + maturity_date = ql.Date( + pd.to_datetime(maturity_date_str).day, + pd.to_datetime(maturity_date_str).month, + pd.to_datetime(maturity_date_str).year, + ) # QuantLib Settings calendar = ql.UnitedStates(ql.UnitedStates.NYSE) # U.S. market calendar (NYSE) day_count = ql.Actual365Fixed() - settlement_date = ql.Date(pd.to_datetime(settlement_date_str).day, - pd.to_datetime(settlement_date_str).month, - pd.to_datetime(settlement_date_str).year) + settlement_date = ql.Date( + pd.to_datetime(settlement_date_str).day, + pd.to_datetime(settlement_date_str).month, + pd.to_datetime(settlement_date_str).year, + ) ql.Settings.instance().evaluationDate = settlement_date # Construct the payoff and the exercise objects - if putcall.upper() == 'P': + if putcall.upper() == "P": right = ql.Option.Put - elif putcall.upper() == 'C': + elif putcall.upper() == "C": right = ql.Option.Call else: raise ValueError(f"Recieved '{putcall}' for putcall. Expected 'P' or 'C'") @@ -1218,8 +1331,8 @@ def optionPV_helper( settlement_date, float(dividend_yield), rate_dc, - ql.Continuous, # make it explicit for dividends - ql.Annual # ignored for continuous but required by signature + ql.Continuous, # make it explicit for dividends + ql.Annual, # ignored for continuous but required by signature ) ) @@ -1228,28 +1341,28 @@ def optionPV_helper( settlement_date, float(risk_free_rate), rate_dc, - ql.Compounded, # common convention for “risk-free” examples - ql.Annual + ql.Compounded, # common convention for “risk-free” examples + ql.Annual, ) ) # risk_free_ts = ql.YieldTermStructureHandle(ql.FlatForward(settlement_date, risk_free_rate, day_count)) - volatility_ts = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(settlement_date, calendar, volatility, day_count)) + volatility_ts = ql.BlackVolTermStructureHandle( + ql.BlackConstantVol(settlement_date, calendar, volatility, day_count) + ) # Black-Scholes-Merton Process (with dividend yield) bsm_process = ql.BlackScholesMertonProcess(spot_handle, dividend_ts, risk_free_ts, volatility_ts) - - - if model == 'mcs': + if model == "mcs": # Monte Carlo Pricing (Longstaff-Schwartz) monte_carlo_engine = ql.MCAmericanEngine(bsm_process, "PseudoRandom", timeSteps=250, requiredSamples=10000) american_option = ql.VanillaOption(payoff, exercise) american_option.setPricingEngine(monte_carlo_engine) monte_carlo_price = american_option.NPV() return monte_carlo_price - - elif model == 'bs': + + elif model == "bs": # Black-Scholes Pricing (Treated as European for comparison) european_exercise = ql.EuropeanExercise(maturity_date) european_option = ql.VanillaOption(payoff, european_exercise) @@ -1259,22 +1372,21 @@ def optionPV_helper( return black_scholes_price except Exception as e: print(f"Error in optionPV_helper: {e}") - logger.info('') + logger.info("") logger.info('"optionPV_helper" raised the below error') logger.info(e) - logger.info(f'Kwargs: {locals()}') + logger.info(f"Kwargs: {locals()}") return 0.0 - def pad_string(input_value): # Convert float to string and remove the decimal point if needed if isinstance(input_value, float): - input_value = str(input_value).replace('.', '') + input_value = str(input_value).replace(".", "") # Convert to string and pad with leading zeros to ensure length of 8 padded_string = str(input_value).zfill(6) - + return padded_string @@ -1294,7 +1406,7 @@ def IV_handler(*args, **kwargs): :param r: risk-free interest rate :type r: float :param q: annualized continuous dividend rate - :type q: float + :type q: float :param flag: 'c' or 'p' for call or put. :type flag: str @@ -1315,15 +1427,15 @@ def IV_handler(*args, **kwargs): >>> abs(expected_price - price) < 0.00001 True >>> abs(expected_iv - iv) < 0.00001 - + """ - keys = ['price', 'S', 'K', 't', 'r', 'q', 'flag'] + keys = ["price", "S", "K", "t", "r", "q", "flag"] if args: extra_kwargs = {k: v for k, v in zip(keys, args)} kwargs.update(extra_kwargs) try: - kwargs['flag'] = kwargs['flag'].lower() + kwargs["flag"] = kwargs["flag"].lower() iv = implied_volatility(**kwargs) if np.isinf(iv): @@ -1331,20 +1443,18 @@ def IV_handler(*args, **kwargs): return iv except (BelowIntrinsicException, ZeroDivisionError) as e: ## Add AboveMaximumException - logger.warning('') + logger.warning("") logger.warning('"implied_volatility" raised the below error') logger.warning(e) - logger.warning(f'Kwargs: {kwargs}') + logger.warning(f"Kwargs: {kwargs}") return 0.0 - + except Exception as j: - logger.warning('') + logger.warning("") logger.warning('"implied_volatility" unrelated error') logger.warning(j) - logger.warning(f'Kwargs: {kwargs}') + logger.warning(f"Kwargs: {kwargs}") return 0.0 - - def binomial_implied_vol(price, S, K, r, exp_date, option_type, pricing_date, dividend_yield): @@ -1379,60 +1489,66 @@ def binomial_implied_vol(price, S, K, r, exp_date, option_type, pricing_date, di >>> iv = binomial_implied_vol(price, S, K, r, exp_date, option_type, pricing_date, dividend_yield) - + """ kwargs = { - 'price': price, - 'S': S, - 'K': K, - 'r': r, - 'T': exp_date, - 'option_type': option_type, - 'pricing_date': pricing_date, - 'dividend_yield': dividend_yield + "price": price, + "S": S, + "K": K, + "r": r, + "T": exp_date, + "option_type": option_type, + "pricing_date": pricing_date, + "dividend_yield": dividend_yield, } try: if price <= 0: - logger.warning('Market price is less than or equal to 0') + logger.warning("Market price is less than or equal to 0") return 0.0 - + return implied_vol_bt( - S0 = S, - K = K, - exp_date = exp_date, - r = r, - y = dividend_yield, - market_price=price, - flag = option_type.lower(), - start = pricing_date + S0=S, + K=K, + exp_date=exp_date, + r=r, + y=dividend_yield, + market_price=price, + flag=option_type.lower(), + start=pricing_date, ) - except Exception as e: - logger.warning('') + logger.warning("") logger.warning('"binomial_implied_vol" raised the below error') logger.warning(e) logger.warning(f"Traceback: {traceback.format_exc()}") - logger.warning(f'Kwargs: {kwargs}') + logger.warning(f"Kwargs: {kwargs}") raise e return 0.0 + def generate_option_tick(symbol, right, exp, strike): - assert right.upper() in ['P', 'C'], f"Recieved '{right}' for right. Expected 'P' or 'C'" - assert isinstance(exp, str), f"Recieved '{type(exp)}' for exp. Expected 'str'" - assert isinstance(strike, ( float)), f"Recieved '{type(strike)}' for strike. Expected 'float'" - - tick_date = pd.to_datetime(exp).strftime('%Y%m%d') - if str(strike)[-1] == '0': + assert right.upper() in ["P", "C"], f"Recieved '{right}' for right. Expected 'P' or 'C'" + assert isinstance(exp, str), f"Recieved '{type(exp)}' for exp. Expected 'str'" + assert isinstance(strike, (float)), f"Recieved '{type(strike)}' for strike. Expected 'float'" + + tick_date = pd.to_datetime(exp).strftime("%Y%m%d") + if str(strike)[-1] == "0": strike = int(strike) else: strike = float(strike) - - key = symbol.upper() + tick_date + pad_string(strike) +right.upper() + + key = symbol.upper() + tick_date + pad_string(strike) + right.upper() return key +class OptionTickComponents(TypedDict): + ticker: str + put_call: str + exp_date: str + strike: float + -def parse_option_tick(tick : str): +def parse_option_tick(tick: str) -> OptionTickComponents: """ Parse the option tick into its components. returns a dictionary with the following keys @@ -1444,47 +1560,43 @@ def parse_option_tick(tick : str): # Regex pattern to extract components pattern = r"([A-Za-z]+)(\d{8})([CP])(\d+(\.\d+)?)" match = re.match(pattern, tick) - + if not match: raise ValueError(f"Invalid option string format, got: {tick}") - + # Extract components from the regex groups ticker = match.group(1) exp_date_raw = match.group(2) put_call = match.group(3) strike = float(match.group(4)) - + # Convert the expiration date to the desired format exp_date = datetime.strptime(exp_date_raw, "%Y%m%d").strftime("%Y-%m-%d") - + # Construct and return the dictionary - return { - "ticker": ticker, - "put_call": put_call, - "exp_date": exp_date, - "strike": strike - } + return {"ticker": ticker, "put_call": put_call, "exp_date": exp_date, "strike": strike} def generate_option_tick_new(symbol, right, exp, strike) -> str: from datetime import datetime - assert right.upper() in ['P', 'C'], f"Recieved '{right}' for right. Expected 'P' or 'C'" - assert isinstance(exp, (str, datetime)), f"Recieved '{type(exp)}' for exp. Expected 'str'" - assert isinstance(strike, ( float)), f"Recieved '{type(strike)}' for strike. Expected 'float'" - - tick_date = pd.to_datetime(exp).strftime('%Y%m%d') - if str(strike)[-1] == '0': + + assert right.upper() in ["P", "C"], f"Recieved '{right}' for right. Expected 'P' or 'C'" + assert isinstance(exp, (str, datetime)), f"Recieved '{type(exp)}' for exp. Expected 'str'" + assert isinstance(strike, (float)), f"Recieved '{type(strike)}' for strike. Expected 'float'" + + tick_date = pd.to_datetime(exp).strftime("%Y%m%d") + if str(strike)[-1] == "0": strike = int(strike) else: strike = float(strike) - - key = symbol.upper() + tick_date + right.upper() + f'{strike}' + + key = symbol.upper() + tick_date + right.upper() + f"{strike}" return key -def wait_for_response(wait_time, condition_func, interval): +def wait_for_response(wait_time, condition_func, interval): ## Can use time.time to ensure it is not counting (meaning not taking func time into consideration) - ## This is better to ensure if it at least reaches 15 secs it ends, rather than 15 secs + loop of time to run + ## This is better to ensure if it at least reaches 15 secs it ends, rather than 15 secs + loop of time to run ## the func call elapsed_time = 0 while elapsed_time < wait_time: @@ -1493,8 +1605,72 @@ def wait_for_response(wait_time, condition_func, interval): time.sleep(interval) elapsed_time += 1 + +def to_datetime( + date_input: str | datetime | pd.Series | list, format: Optional[str] = None +) -> datetime | pd.DatetimeIndex: + """ + Convert a string or iterable to datetime object(s). + If input is already a datetime object, return as is. + For iterables, uses pd.to_datetime. + + Args: + date_input: String, datetime, or iterable to convert. + format: Optional strftime format. If None, tries "%Y-%m-%d" first, then lets pandas guess. + + Returns: + datetime object for single input, DatetimeIndex for iterables. + + Raises: + ValueError: If conversion fails with all attempted methods. + """ + # Return datetime objects as-is + if isinstance(date_input, (datetime)): + return date_input + + elif isinstance(date_input, pd.Timestamp): + return date_input.to_pydatetime() + + elif isinstance(date_input, np.datetime64): + return pd.to_datetime(date_input).to_pydatetime() + + elif isinstance(date_input, date): + return datetime(date_input.year, date_input.month, date_input.day) + + # Handle iterables (list, tuple, pd.Series, etc.) + if hasattr(date_input, "__iter__") and not isinstance(date_input, str): + if format: + return pd.to_datetime(date_input, format=format) + else: + # Try standard format first + try: + return pd.to_datetime(date_input, format="%Y-%m-%d") + except (ValueError, TypeError): + # Let pandas guess the format + return pd.to_datetime(date_input) + + # Handle single string input + if format: + return datetime.strptime(date_input, format) + + # Try standard format first for speed + try: + return datetime.strptime(date_input, "%Y-%m-%d") + except ValueError: + # Let pandas guess the format + result = pd.to_datetime(date_input) + # Convert pandas Timestamp to datetime + if isinstance(result, pd.Timestamp): + return result.to_pydatetime() + return result + + def is_busday(date): - return bool(len(pd.bdate_range(date, date))) + """ + Returns True if the date is a business day, False otherwise + """ + date = to_datetime(date) + return date.weekday() < 5 def is_USholiday(date): @@ -1502,12 +1678,11 @@ def is_USholiday(date): Returns True if the date is a US holiday, False otherwise """ - # import holidays - import pandas_market_calendars as mcal - date = pd.to_datetime(date) - return date.date().strftime('%Y-%m-%d') in HOLIDAY_SET + date = to_datetime(date) + return date.date().strftime("%Y-%m-%d") in HOLIDAY_SET + -def not_trading_day(date: str|datetime, time_aware: bool = False) -> bool: +def not_trading_day(date: str | datetime, time_aware: bool = False) -> bool: """ Returns True if the date is not a trading day (weekend or holiday), False otherwise If time_aware is True, also checks if the time is outside of trading hours (9:30 - 16:00) @@ -1519,12 +1694,12 @@ def not_trading_day(date: str|datetime, time_aware: bool = False) -> bool: """ conf = get_pricing_config() ret_bool = not is_busday(date) or is_USholiday(date) - open_time = pd.Timestamp(conf['MARKET_OPEN_TIME']).time() - close_time = pd.Timestamp(conf['MARKET_CLOSE_TIME']).time() - + open_time = pd.Timestamp(conf["MARKET_OPEN_TIME"]).time() + close_time = pd.Timestamp(conf["MARKET_CLOSE_TIME"]).time() + if not time_aware: return ret_bool - + ## If today, check if today is 9:30 <= time <= 16:00 if pd.to_datetime(date).date() == datetime.today().date(): if open_time <= pd.to_datetime(date).time() <= close_time: @@ -1533,16 +1708,18 @@ def not_trading_day(date: str|datetime, time_aware: bool = False) -> bool: ret_bool = True ## Time Check only if time != 00:00:00 - elif pd.to_datetime(date).time() != pd.Timestamp('00:00:00').time(): + elif pd.to_datetime(date).time() != pd.Timestamp("00:00:00").time(): if pd.to_datetime(date).time() < open_time or pd.to_datetime(date).time() > close_time: ret_bool = True else: ret_bool = False return ret_bool - -def change_to_last_busday(end, offset = 1): +def change_to_last_busday(end, + offset=1, + eod_time=True, + time_of_day_aware: bool = True) -> datetime: """ Change the end date to the last business day if it falls on a weekend or holiday. If the time is before 9:30, move to the previous business day. @@ -1553,43 +1730,56 @@ def change_to_last_busday(end, offset = 1): if offset < 0 it will move forward if offset = 0 it will stay on the same day if it is a business day if offset > 0 it will move back + eod_time: bool, if True, return the end date at 16:00:00, else at 00:00:00 + time_of_day_aware: bool, + if True: + - If time is missing (00:00:00), default to 16:00:00 + - If time is before 9:30, move to previous business day at 16:00:00 + - If time is after 16:00, move to same business day at 16:00:00 + if False: + - Ignore time of day, always return date at 00:00:00 + returns: datetime """ - - #Enfore time is passed + # Enfore time is passed if not isinstance(end, str): - end = end.strftime('%Y-%m-%d %H:%M:%S') + end = end.strftime("%Y-%m-%d %H:%M:%S") + + if pd.to_datetime(end).time() == pd.Timestamp("00:00:00").time() and time_of_day_aware: + end = end + " 16:00:00" - if pd.to_datetime(end).time() == pd.Timestamp('00:00:00').time(): - end = end + ' 16:00:00' - ## Make End Comparison Busday isBiz = is_busday(end) while not isBiz: end_dt = pd.to_datetime(end) - end_dt = end_dt.replace(hour=16, minute=0, second=0) ## Defaulting to EOD - end = (end_dt - BDay( offset)).strftime('%Y-%m-%d %H:%M:%S') + end_dt = end_dt.replace(hour=16, minute=0, second=0) ## Defaulting to EOD + end = (end_dt - BDay(offset)).strftime("%Y-%m-%d %H:%M:%S") isBiz = bool(len(pd.bdate_range(end, end))) ## Make End Comparison prev day if before 9:30 - if pd.Timestamp(end).time() = pd.Timestamp('16:00').time(): + elif pd.Timestamp(end).time() >= pd.Timestamp("16:00").time() and time_of_day_aware: end_dt = pd.to_datetime(end) - end = end_dt.replace(hour=16, minute=0, second=0).strftime('%Y-%m-%d %H:%M:%S') - + end = end_dt.replace(hour=16, minute=0, second=0).strftime("%Y-%m-%d %H:%M:%S") # Make End Comparison prev day if holiday while is_USholiday(end): end_dt = pd.to_datetime(end) - end = (end_dt - BDay(offset)).strftime('%Y-%m-%d %H:%M:%S') + end = (end_dt - BDay(offset)).strftime("%Y-%m-%d %H:%M:%S") + + if not eod_time: + end = to_datetime(end) + end = end.replace(hour=0, minute=0, second=0, microsecond=0) + return end + + return datetime.strptime(end, "%Y-%m-%d %H:%M:%S") - return pd.to_datetime(end) def is_class_method(cls, obj): """ diff --git a/trade/helpers/helper_types.py b/trade/helpers/helper_types.py index b56f6d3..9cc6350 100644 --- a/trade/helpers/helper_types.py +++ b/trade/helpers/helper_types.py @@ -1,40 +1,136 @@ -from typing import TypedDict +from dataclasses import fields +from typing import Iterable, TypedDict, Any from enum import Enum +from datetime import datetime from abc import ABC, abstractmethod from typing import ClassVar from weakref import WeakSet from trade.helpers.exception import SymbolChangeError +from typing import get_origin, get_args, Union, get_type_hints, Literal +import types +from trade.helpers.Logging import setup_logger + +logger = setup_logger(__name__) +DATE_HINT = Union[datetime, str] + +class IncorrectTypeError(Exception): + """Custom exception for incorrect type errors in configuration validation.""" + + pass + + +def validate_inputs(self: object, raise_on_fail: bool = False) -> None: + type_hints = get_type_hints(type(self)) + + for f in fields(self): + try: + field_name = f.name + field_value = getattr(self, field_name) + + type_hint = type_hints.get(field_name) + if type_hint is None: + continue # no annotation, skip + + origin = get_origin(type_hint) + args = get_args(type_hint) + + # --- Handle Literal[...] --- + if origin is Literal: + # e.g. name: Literal["LimitsCog", "OtherCog"] + allowed_values = args # tuple of literals + + if field_value is None: + # If you want to allow None here, add it to the Literal. + logger.warning(f"Configuration '{field_name}' is None but expected one of {allowed_values}.") + elif field_value not in allowed_values: + raise IncorrectTypeError( + f"Configuration '{field_name}' expected one of {allowed_values}, " f"but got {field_value!r}." + ) + continue + + # --- Handle Optional / Union[...] --- + if origin in (Union, types.UnionType): + allows_none = any(arg is type(None) for arg in args) + if field_value is None: + if not allows_none: + logger.warning( + f"Configuration '{field_name}' is not set (None) and is not Optional. Please review." + ) + continue + + valid_types = tuple(arg for arg in args if arg is not type(None)) + if not isinstance(field_value, valid_types): + raise IncorrectTypeError( + f"Configuration '{field_name}' expected types {valid_types}, " f"but got {type(field_value)}." + ) + continue + + # --- Simple (non-generic) types --- + if origin is None: + if field_value is None: + logger.warning(f"Configuration '{field_name}' is not set (None). Please review.") + continue + + if not isinstance(field_value, type_hint): + raise IncorrectTypeError( + f"Configuration '{field_name}' expected type {type_hint}, " f"but got {type(field_value)}." + ) + continue + + # --- Other generics (List, Dict, etc.) – shallow check --- + if field_value is None: + logger.warning(f"Configuration '{field_name}' is not set (None). Please review.") + continue + + try: + if not isinstance(field_value, origin): + raise IncorrectTypeError( + f"Configuration '{field_name}' expected type {origin}, " f"but got {type(field_value)}." + ) + except TypeError: + logger.warning( + f"Could not validate field '{field_name}' with value '{field_value}' against type '{type_hint}' due to TypeError." + ) + pass + + except Exception as e: + logger.critical(f"Failed to validate field '{f.name}' in {self.__class__.__name__}. Error: {e}") + if raise_on_fail: + raise e + class OptionTickMetaData(TypedDict): ticker: str exp_date: str put_call: str strike: float - -class PositionData(TypedDict): + + +class PositionData(TypedDict): long: list[str] short: list[str] - class OptionModelAttributes(Enum): - S0 = 'unadjusted_S0' - K = 'K' - exp_date = 'exp' - sigma = 'sigma' - y = 'y' - put_call = 'put_call' - r = 'rf_rate' - start = 'end_date' - spot_type = 'chain_price' - + S0 = "unadjusted_S0" + K = "K" + exp_date = "exp" + sigma = "sigma" + y = "y" + put_call = "put_call" + r = "rf_rate" + start = "end_date" + spot_type = "chain_price" class TickerMap(dict): - invalid_tickers = {'FB': 'META'} + invalid_tickers = {"FB": "META"} + def __getitem__(self, key): if key in self.invalid_tickers: - raise SymbolChangeError(f"Tick name changed from {key} to {self.invalid_tickers[key]}, access the new tick instead") + raise SymbolChangeError( + f"Tick name changed from {key} to {self.invalid_tickers[key]}, access the new tick instead" + ) return super().__getitem__(key) @@ -50,7 +146,6 @@ def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) SingletonMixin._registry.add(cls) - @classmethod @abstractmethod def clear_instances(cls): @@ -76,10 +171,19 @@ class SingletonMetaClass(type): A metaclass for singleton classes. It ensures that only one instance of a class is created. """ + _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: instance = super().__call__(*args, **kwargs) cls._instances[cls] = instance - return cls._instances[cls] \ No newline at end of file + return cls._instances[cls] + + +def is_iterable(obj: Any, include_str: bool = False) -> bool: + """Check if an object is iterable, optionally excluding strings.""" + if include_str: + return isinstance(obj, Iterable) + else: + return isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)) \ No newline at end of file diff --git a/trade/helpers/vars.py b/trade/helpers/vars.py new file mode 100644 index 0000000..8379835 --- /dev/null +++ b/trade/helpers/vars.py @@ -0,0 +1,36 @@ +from typing import List, Callable +_CUSTOM_ON_EXIT_BUCKET: List[Callable] = [] + +def register_on_exit(handler: Callable) -> None: + """ + Register a function to be called upon program exit. + + Parameters: + ---------- + handler : Callable + The function to be called on exit. + """ + global _CUSTOM_ON_EXIT_BUCKET + _CUSTOM_ON_EXIT_BUCKET.append(handler) + +def run_on_exit_handlers() -> None: + """Run all registered on-exit handlers.""" + global _CUSTOM_ON_EXIT_BUCKET + for handler in _CUSTOM_ON_EXIT_BUCKET: + try: + handler() + except Exception as e: + print(f"Error running on-exit handler {handler.__name__}: {e}") + +def get_on_exit_bucket() -> List[Callable]: + """Get the list of registered on-exit handlers.""" + global _CUSTOM_ON_EXIT_BUCKET + return _CUSTOM_ON_EXIT_BUCKET + +def clear_on_exit_bucket() -> None: + """Clear the list of registered on-exit handlers.""" + global _CUSTOM_ON_EXIT_BUCKET + _CUSTOM_ON_EXIT_BUCKET = [] + +SECONDS_IN_YEAR = 365.0 * 24.0 * 3600.0 +SECONDS_IN_DAY = 24.0 * 3600.0 \ No newline at end of file diff --git a/trade/models/VolSurface.py b/trade/models/VolSurface.py index 65fd560..aca7b27 100644 --- a/trade/models/VolSurface.py +++ b/trade/models/VolSurface.py @@ -140,7 +140,7 @@ def fit_svi_model( price = x['price'], S = x['Spot'], K = x['Strike'], - t = time_distance_helper(exp = x['Expiration'].strftime('%Y-%m-%d'), strt = x.name.strftime('%Y-%m-%d')), + t = time_distance_helper(end = x['Expiration'].strftime('%Y-%m-%d'), start = x.name.strftime('%Y-%m-%d')), r = x['r'], q = x['q'], flag = x['right'].lower()), axis = 1) diff --git a/trade/models/utils.py b/trade/models/utils.py index 17272ee..89ec378 100644 --- a/trade/models/utils.py +++ b/trade/models/utils.py @@ -115,7 +115,7 @@ def resolve_missing_vol( price = x['Midpoint'], S = S, K = x['strike'], - t = time_distance_helper(exp = expiration, strt = datetime), + t = time_distance_helper(end = expiration, start = datetime), r = r, q = q, flag = x['right'].lower()), axis = 1) @@ -124,7 +124,7 @@ def resolve_missing_vol( price = x['Close'], S = S, K = x['strike'], - t = time_distance_helper(exp = expiration, strt = datetime), + t = time_distance_helper(end = expiration, start = datetime), r = r, q = q, flag = x['right'].lower()), axis = 1) diff --git a/trade/optionlib/assets/dividend.py b/trade/optionlib/assets/dividend.py index 57d0418..fbaabda 100644 --- a/trade/optionlib/assets/dividend.py +++ b/trade/optionlib/assets/dividend.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from datetime import datetime -from typing import List, Tuple, Union, Any +from typing import List, Tuple, Union, Any, Iterable import math from dateutil.rrule import rrule, MONTHLY from dateutil.relativedelta import relativedelta @@ -18,10 +18,10 @@ from ..utils.timing import format_dates, subtract_dates, validate_dates from ..config.defaults import DAILY_BASIS, DIVIDEND_LOOKBACK_YEARS, DIVIDEND_FORECAST_METHOD from trade.helpers.Logging import setup_logger +from trade.helpers.vars import SECONDS_IN_DAY, SECONDS_IN_YEAR # noqa +from trade.helpers.helper_types import DATE_HINT -logger = setup_logger("trade.optionlib.assets.dividend") - - +logger = setup_logger("trade.optionlib.assets.dividend", stream_log_level="DEBUG") FREQ_MAP = { "monthly": 1, "quarterly": 3, @@ -205,13 +205,14 @@ def _dual_project_dividends( """ end_date, valuation_date = format_dates(end_date, valuation_date) typical_spacing = div_history.index.to_series().diff().dt.days.mode()[0] - expected_dividend_size = int((subtract_dates(end_date, valuation_date) // typical_spacing) + 1) - logger.info(f"Expected Dividend Size before adjustment: {expected_dividend_size}") period_inferred = classify_frequency(typical_spacing) + ## Push back valuation date by period & typical spacing * 2 to capture historical dividends + new_valuation_date = valuation_date - relativedelta(days=typical_spacing * 8) + ## Get dividends btwn valuation date and today historical_divs = div_history.loc[ - (div_history.index.date >= valuation_date.date()) + (div_history.index.date >= new_valuation_date.date()) & ## Filter to include only dividends between valuation date and today. With today inclusive (div_history.index.date <= datetime.today().date()) @@ -222,6 +223,16 @@ def _dual_project_dividends( if not date_list: return [], [], valuation_date + ## Expected dividend size: + ## Since we pushed valuation date back, we will include it in expected dividend size calculation + expected_dividend_size = int((subtract_dates(end_date, new_valuation_date) // typical_spacing) + 1) + expected_dividend_size_for_original_valuation = int( + (subtract_dates(end_date, valuation_date) // typical_spacing) + 1 + ) + logger.info( + f"Expected Dividend Size before adjustment: {expected_dividend_size}, for original valuation: {expected_dividend_size_for_original_valuation}. Size from historical divs: {len(date_list)}" + ) + ## Project future dividends after today last_div = amount_list[-1] if amount_list else 0.0 @@ -230,19 +241,27 @@ def _dual_project_dividends( ## We reduce expected dividend size by the number of historical dividends we have expected_dividend_size -= len(date_list) - logger.info(f"Expected Dividend Size after adjustment: {expected_dividend_size}") + + ## If expected dividend size is less than 0, set to 0 + if expected_dividend_size < 0: + expected_dividend_size = 0 + + logger.info(f"Expected Dividend Size to be projected: {expected_dividend_size}") periodic_growth = inferred_growth_rate / (12 / FREQ_MAP[period_inferred]) ## Generate projected dividends starting from last_date dividend_list = [last_div * (1 + periodic_growth) ** i for i in range(expected_dividend_size)] + logger.info(f"Projected Dividend List: {dividend_list}") ## Combine historical and projected dividends dividend_list = amount_list + dividend_list + logger.info(f"Combined Dividend List: {dividend_list}") ## Combine historical and projected dates date_list = date_list + [ last_date + relativedelta(months=i * FREQ_MAP[period_inferred]) for i in range(1, expected_dividend_size + 1, 1) ] + logger.info(f"Combined Date List: {date_list}") ## Cutoff any dates beyond end_date filtered_dividends = [ @@ -302,13 +321,19 @@ def date(self) -> datetime: def amount(self) -> float: return self[1] + def __mul__(self, value) -> "ScheduleEntry": + if not isinstance(value, (int, float)): + raise TypeError(f"Can only multiply ScheduleEntry by int or float, not {type(value)}") + return ScheduleEntry(self.date, self.amount * value) + def __repr__(self) -> str: - return f"" + use_date = self.date.strftime('%Y-%m-%d') if isinstance(self.date, datetime) else str(self.date) + return f"" class Schedule: """ - Class to represent a dividend schedule. + Class to represent a dividend schedule for a given date. """ def __init__(self, schedule: List[Tuple[datetime, float]]): @@ -316,7 +341,7 @@ def __init__(self, schedule: List[Tuple[datetime, float]]): Initialize a Schedule object. schedule: List[Tuple[datetime, float]] - A list of tuples containing dividend dates and amounts. """ - self._schedule = schedule + self._schedule: List[Tuple[datetime, float]] = schedule @property def schedule(self) -> List[ScheduleEntry]: @@ -353,6 +378,18 @@ def __str__(self): """ return self.__repr__() + def __mul__(self, value: float) -> "Schedule": + """ + Multiply all amounts in the schedule by a scalar value. + value: float - The scalar value to multiply by. + Returns: + Schedule: A new Schedule object with updated amounts. + """ + if not isinstance(value, (int, float)): + raise TypeError(f"Can only multiply Schedule by int or float, not {type(value)}") + new_schedule = [(entry.date, entry.amount * value) for entry in self.schedule] + return Schedule(new_schedule) + def __iter__(self): """ Make the Schedule object iterable. @@ -432,7 +469,7 @@ def get_year_fractions(self) -> List[Tuple[float, float]]: """ return Schedule( [ - (time_distance_helper(dt, self.valuation_date), amt) + (time_distance_helper(end=dt, start=self.valuation_date), amt) for dt, amt in self.schedule if dt > self.valuation_date ] @@ -446,7 +483,7 @@ def get_present_value(self, discount_rate: float, sum_up: bool = True, **kwargs) pv = [] for dt, amt in self.schedule: if compare_dates.is_after(dt, self.valuation_date): - time_fraction = time_distance_helper(dt, self.valuation_date) + time_fraction = time_distance_helper(end=dt, start=self.valuation_date) pv_amt = amt * math.exp(-discount_rate * time_fraction) pv.append(pv_amt) return sum(pv) if sum_up else pv @@ -485,8 +522,8 @@ def __init__( self.valuation_date = valuation_date or start_date self.end_date = end_date self.T = time_distance_helper( - self.end_date, - self.valuation_date, + end=self.end_date, + start=self.valuation_date, ) def get_yield(self) -> float: @@ -502,7 +539,7 @@ def get_present_value(self, end_date: datetime = None, **kwargs) -> float: Return the exponential discount factor from q over T: e^{-qT} """ - T = self.T if end_date is None else time_distance_helper(end_date, self.valuation_date) + T = self.T if end_date is None else time_distance_helper(end=end_date, start=self.valuation_date) return math.exp(-self.yield_rate * T) def get_type(self) -> str: @@ -782,23 +819,29 @@ def vector_convert_to_time_frac( Returns a list of lists containing time fractions and amounts. """ - assert_equal_length(schedules, valuation_dates, end_dates) - time_fractions = [] - for i, sch in enumerate(schedules): - time_fractions.append( - Schedule( - [ - (time_distance_helper(dt, valuation_dates[i]), amt) - for amt, dt in sch - if compare_dates.is_after(dt, valuation_dates[i]) - ] - ) - ) - return time_fractions + # assert_equal_length(schedules, valuation_dates, end_dates) + + out: List[Schedule] = [] + + for sch, val, end in zip(schedules, valuation_dates, end_dates): + # Convert once + + # If schedule dates are sorted, you can optionally early-break (see note below) + converted = [] + for dt, amt in sch: # dt is datetime, amt is float + # Exclusive bounds: val < dt < end + days_in_seconds = (dt - val.date()).days * 86400 + if val.date() < dt < end.date(): + t = days_in_seconds / SECONDS_IN_YEAR + converted.append((t, amt)) + + out.append(Schedule(converted)) + + return out def vectorized_discrete_pv( - schedules: List[list], r: List[list], _valuation_dates: List[datetime], _end_dates: List[datetime] + schedules: List[List[ScheduleEntry]], r: List[list], _valuation_dates: List[datetime], _end_dates: List[datetime] ) -> List[float]: """ Calculate the present value of a list of dividend schedules using vectorized operations. @@ -808,21 +851,42 @@ def vectorized_discrete_pv( _end_dates: List[datetime] - List of end dates corresponding to each schedule. Returns a list of present values for each schedule. """ - assert_equal_length(schedules, r, _end_dates, _valuation_dates) + assert_equal_length( + schedules, r, _end_dates, _valuation_dates, names=["schedules", "r", "_end_dates", "_valuation_dates"] + ) + df_cache = {} + pv = [] + SECONDS_IN_YEAR = 365.0 * 24.0 * 3600.0 + for i, sch in enumerate(schedules): - pv.append( - sum( - [ ## Calculating the sum - (x * math.exp(-r[i] * time_distance_helper(dt, _valuation_dates[i]))) ## Applying discount factor - for x, dt in sch - if compare_dates.inbetween( - dt, start=_valuation_dates[i], end=_end_dates[i], inclusive=False - ) ## Filtering for dt after Val - ] - ) - ) - return pv[0] if len(pv) == 1 else pv + ri = r[i] # rate for this schedule + val = _valuation_dates[i] + end = _end_dates[i] + + # Use integer seconds + val_ts = int(val.timestamp()) + + total = 0.0 + + # sch entries are (date, div) per your point (2) + for dt, x in sch: + if val.date() < dt < end.date(): + days_in_seconds = (dt - val.date()).days * 86400 + + key = (ri, val_ts, days_in_seconds) + df = df_cache.get(key) + + if df is None: + t = days_in_seconds / SECONDS_IN_YEAR + df = math.exp(-ri * t) + df_cache[key] = df + + total += x * df + + pv.append(total) + + return pv def get_vectorized_dividend_rate(tickers: str | List[str], spots: List[float], valuation_dates: List[float]): @@ -842,8 +906,10 @@ def get_vectorized_dividend_rate(tickers: str | List[str], spots: List[float], v def get_vectorized_continuous_dividends( - div_rates: List[float], _valuation_dates: List[datetime], _end_dates: List[datetime] -): + div_rates: Iterable[float], + _valuation_dates: Iterable[DATE_HINT], + _end_dates: Iterable[DATE_HINT], +) -> np.ndarray: """ Get the vectorized continuous dividend discount factors. div_rates: List[float] - List of continuous dividend rates. @@ -851,19 +917,11 @@ def get_vectorized_continuous_dividends( _end_dates: List[datetime] - List of end dates. Returns a numpy array of discount factors. """ - - assert_equal_length( - div_rates, - _valuation_dates, + div_rates = np.array(div_rates, dtype=float) + _valuation_dates = np.array(_valuation_dates, dtype='datetime64[D]') + _end_dates = np.array(_end_dates, dtype='datetime64[D]') + t = time_distance_helper( + start=_valuation_dates, + end=_end_dates, ) - discounted = [ - math.exp( - -div_rate - * time_distance_helper( - _end_dates[i], - _valuation_dates[i], - ) - ) - for i, div_rate in enumerate(div_rates) - ] - return np.array(discounted) + return np.exp(-div_rates * t) diff --git a/trade/optionlib/assets/forward.py b/trade/optionlib/assets/forward.py index 4d2cdb4..98dcdeb 100644 --- a/trade/optionlib/assets/forward.py +++ b/trade/optionlib/assets/forward.py @@ -171,7 +171,7 @@ def get_forward_price(self) -> float: q = dividend yield T = time to maturity in years """ - T = time_distance_helper(self.end_date, self.valuation_date) + T = time_distance_helper(end=self.end_date, start=self.valuation_date) if T <= 0: raise ValueError("End date must be after valuation date.") @@ -335,12 +335,12 @@ def vectorized_forward_discrete(S, r, T, pv_divs): T: time to maturity (array) pv_divs: Summation of present value of all dividends till end date """ - assert_equal_length(S, r, pv_divs, T) + assert_equal_length(S, r, pv_divs, T, names=['S', 'r', 'pv_divs', 'T']) S, r, T, pv_divs = convert_to_array(S, r, T, pv_divs) forward = (S - pv_divs) * np.exp(r * T) return forward - +# TODO: Rework on this function. I need to include back-adjusted dividends. def vectorized_market_forward_calc(ticks: List[str], S: List[float], valuation_dates: List[datetime], @@ -377,7 +377,7 @@ def vectorized_market_forward_calc(ticks: List[str], F = vectorized_forward_discrete( S=S, r=r, - T=[time_distance_helper(end_dates[i], valuation_dates[i]) for i in range(len(end_dates))], + T=[time_distance_helper(end=end_dates[i], start=valuation_dates[i]) for i in range(len(end_dates))], pv_divs=div_amt ) @@ -397,7 +397,7 @@ def vectorized_market_forward_calc(ticks: List[str], S=S, r=r, q_factor=div_amt, - T=[time_distance_helper(end_dates[i], valuation_dates[i]) for i in range(len(end_dates))] + T=[time_distance_helper(end=end_dates[i], start=valuation_dates[i]) for i in range(len(end_dates))] ) div_amt = (div_rate, div_amt) # Return the dividend rate and present value of dividends diff --git a/trade/optionlib/config/config.yaml b/trade/optionlib/config/config.yaml index c5d39f6..2c196fe 100644 --- a/trade/optionlib/config/config.yaml +++ b/trade/optionlib/config/config.yaml @@ -2,7 +2,7 @@ DIVIDEND_FORECAST_LOOKBACK_YEARS: 1 DIVIDEND_FORECAST_METHOD: "avg" DIVIDEND_FORECAST_LOOKFORWARD_YEARS: 4 DAILY_BASIS: 365.25 -OPTION_TIMESERIES_START_DATE: "2017-01-01" +OPTION_TIMESERIES_START_DATE: "2018-01-01" VOL_EST_UPPER_BOUND: 5.0 VOL_EST_LOWER_BOUND: 0.01 N_PRECISION_GREEKS: 200 diff --git a/trade/optionlib/core/black_scholes_math.py b/trade/optionlib/core/black_scholes_math.py index f3a43f9..b8687e8 100644 --- a/trade/optionlib/core/black_scholes_math.py +++ b/trade/optionlib/core/black_scholes_math.py @@ -66,13 +66,14 @@ def black_scholes_vectorized_base(F: np.ndarray|List[float], Returns: Option prices (array) """ - # Ensure all inputs are numpy arrays for vectorized operations - F = np.asarray(F) - K = np.asarray(K) - T = np.asarray(T) - r = np.asarray(r) - sigma = np.asarray(sigma) + # Ensure all inputs are numpy arrays for vectorized operations + F = np.asarray(F, dtype=np.float64) + K = np.asarray(K, dtype=np.float64) + T = np.asarray(T, dtype=np.float64) + r = np.asarray(r, dtype=np.float64) + sigma = np.asarray(sigma, dtype=np.float64) + d1 = (np.log(F / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T)) d2 = d1 - sigma * np.sqrt(T) df = np.exp(-r * T) diff --git a/trade/optionlib/greeks/__init__.py b/trade/optionlib/greeks/__init__.py index 09b9446..b6ff693 100644 --- a/trade/optionlib/greeks/__init__.py +++ b/trade/optionlib/greeks/__init__.py @@ -40,7 +40,7 @@ def vectorized_market_greeks_bsm( raise ValueError("option_type must be a single string or a list of strings with the same length as ticks.") # Convert valuation_dates and end_dates to Timedelta - T = [time_distance_helper(end_dates[i], valuation_dates[i]) for i in range(len(end_dates))] + T = [time_distance_helper(end=end_dates[i], start=valuation_dates[i]) for i in range(len(end_dates))] # Calculate the Greeks using the specified style if greek_style == 'analytic': diff --git a/trade/optionlib/greeks/analytical/black_scholes.py b/trade/optionlib/greeks/analytical/black_scholes.py index 5e97b92..966027a 100644 --- a/trade/optionlib/greeks/analytical/black_scholes.py +++ b/trade/optionlib/greeks/analytical/black_scholes.py @@ -36,7 +36,7 @@ def _ptched_bsm_for_analytical( r=r, div_type=div_type ) - T = [time_distance_helper(end_dates[i], valuation_dates[i]) for i in range(len(end_dates))] + T = [time_distance_helper(end=end_dates[i], start=valuation_dates[i]) for i in range(len(end_dates))] greeks = black_scholes_analytic_greeks_vectorized( F=F, K=K, diff --git a/trade/optionlib/greeks/numerical/binomial.py b/trade/optionlib/greeks/numerical/binomial.py index 5c58098..9f98b80 100644 --- a/trade/optionlib/greeks/numerical/binomial.py +++ b/trade/optionlib/greeks/numerical/binomial.py @@ -1,5 +1,6 @@ +from typing import Iterable, List +from trade.optionlib.assets.dividend import Schedule import numpy as np -from typing import Union, List from ...utils.format import convert_to_array, assert_equal_length,equalize_lengths from ...pricing.binomial import VectorBinomialCRR from trade.helpers.Logging import setup_logger @@ -13,7 +14,7 @@ def binomial_tree_price_batch( N: float|np.ndarray, S: float|np.ndarray, dividend_type: float|np.ndarray, - div_amount: float|np.ndarray, + div_amount: List[Schedule] | Iterable[float] | np.ndarray, option_type: float|np.ndarray, start_date: float|np.ndarray, valuation_date: float|np.ndarray, @@ -75,16 +76,6 @@ def binomial_tree_price_batch( ) ] price = np.array([model.price() for model in models]) - # price = [] - # for i, model in enumerate(models): - # try: - # print(f"Model {i}: K={model.K}, S={model.S0}, N={model.N}, T={model.T}, option_type={model.option_type}") - # price.append(model.price()) - # except Exception as e: - # print(f"Error in model {i}: {e}") - # print(model.stock_tree) - # print(model, ) - # raise price = np.array(price) return price, models @@ -97,7 +88,7 @@ def binomial_tree_greeks( N: float|np.ndarray, S: float|np.ndarray, dividend_type: float|np.ndarray, - div_amount: float|np.ndarray, + div_amount: List[Schedule] | Iterable[float] | np.ndarray, option_type: float|np.ndarray, start_date: float|np.ndarray, valuation_date: float|np.ndarray, @@ -114,6 +105,8 @@ def binomial_tree_greeks( - N: Number of time steps in the binomial tree - spot_price: Current price of the underlying asset (optional) - dividend_type: Type of dividend ('discrete' or 'continuous') + - Discrete dividends: (time_frac, amount) schedule + - Continuous dividends: continuous yield rate - div_amount: Amount of dividend (if applicable) - option_type: 'c' for call, 'p' for put - start_date: Start date for the option pricing (optional) diff --git a/trade/optionlib/greeks/numerical/black_scholes.py b/trade/optionlib/greeks/numerical/black_scholes.py index f49b9e0..c92dd8a 100644 --- a/trade/optionlib/greeks/numerical/black_scholes.py +++ b/trade/optionlib/greeks/numerical/black_scholes.py @@ -1,10 +1,7 @@ from datetime import datetime import numpy as np from typing import List, Union -from scipy.stats import norm -from ...config.defaults import DAILY_BASIS from ...core.black_scholes_math import ( - black_scholes_analytic_greeks_vectorized, black_scholes_vectorized_base ) from ...assets.forward import ( @@ -118,7 +115,7 @@ def vectorized_black_scholes_greeks( r: List[float], sigma: List[float], option_type: str|List[str] = "c", - div_type='discrete', + dividend_type='discrete', div_amount=None) -> dict: """ Vectorized Black-Scholes Greeks calculation. @@ -130,29 +127,31 @@ def vectorized_black_scholes_greeks( r: Risk-free rates (annualized, array) sigma: Volatilities (annualized, array) option_type: "c" for call, "p" for put (single string or list of strings) - div_type: Type of dividend ('discrete' or 'continuous') + dividend_type: Type of dividend ('discrete' or 'continuous') div_amount: Dividend amount (single float or list of floats, ignored for continuous dividends) - if discrete expecting present value of discrete dividends, else if continuous expecting continuous yield rate. + if discrete expecting present value of discrete dividends, else if continuous expecting discounted continuous yield rate. Returns: Greeks (dictionary) """ - T = [time_distance_helper(end_dates[i], valuation_dates[i]) for i in range(len(end_dates))] + T = time_distance_helper(start=valuation_dates, end=end_dates) finite_estimator = FiniteGreeksEstimator( price_func=_ptched_bsm_for_numerical, base_params={ - 'F': np.asarray(F), - 'K': np.asarray(K), - 'T': np.asarray(T), - 'r': np.asarray(r), - 'sigma': np.asarray(sigma), - 'q': 0.0, # Assuming no continuous dividend yield for simplicity - 'S': np.asarray(S), # Including spot price for delta calculation - 'option_type': option_type, - 'div_type': div_type, - 'div_amount': div_amount # Placeholder, will be ignored in the patched function + "F": np.asarray(F), + "K": np.asarray(K), + "T": np.asarray(T), + "r": np.asarray(r), + "sigma": np.asarray(sigma), + # Assuming no continuous dividend yield for simplicity + # We pass continuous yield in div_amount if div_type is 'continuous' + "q": 0.0, + "S": np.asarray(S), # Including spot price for delta calculation + "option_type": option_type, + "dividend_type": dividend_type, + "div_amount": div_amount, # Placeholder, will be ignored in the patched function }, dx_thresh=0.00001, - method='central' # Use backward method for finite differences + method="central", # Use backward method for finite differences ) greeks = finite_estimator.all_first_order() greeks.update(finite_estimator.all_second_order()) diff --git a/trade/optionlib/pricing/binomial.py b/trade/optionlib/pricing/binomial.py index 916ef06..eb405d9 100644 --- a/trade/optionlib/pricing/binomial.py +++ b/trade/optionlib/pricing/binomial.py @@ -2,38 +2,33 @@ from datetime import datetime from typing import List import numpy as np -from typing import Tuple +from typing import Tuple, Iterable from numba import njit from numba import types +from numba.typed import List as _List from trade.helpers.Logging import setup_logger from trade.helpers.helper import Scalar +from trade.helpers.threads import runThreads +from ..utils.format import assert_equal_length from ..config.defaults import DAILY_BASIS from ..assets.forward import time_distance_helper from ..assets.dividend import ( DividendSchedule, ContinuousDividendYield, ) -from ..assets.forward import ( - EquityForward -) -logger = setup_logger('trade.optionlib.pricing.binomial') +from ..assets.forward import EquityForward + +logger = setup_logger("trade.optionlib.pricing.binomial") def convert_schedule_to_numba(schedule: list[tuple[float, float]]) -> List: - lst = List.empty_list(types.UniTuple(types.float64, 2)) + lst = _List.empty_list(types.UniTuple(types.float64, 2)) for t_frac, amount in schedule: lst.append((float(t_frac), float(amount))) return lst -def crr_init_parameters( - sigma: float, - r: float, - T: float, - N: int, - div_yield: float = 0.0, - dividend_type: bool = True -): +def crr_init_parameters(sigma: float, r: float, T: float, N: int, div_yield: float = 0.0, dividend_type: bool = True): """ params: sigma: Volatility of the underlying asset @@ -43,24 +38,12 @@ def crr_init_parameters( dividend_type: Type of dividend ('discrete' or 'continuous' """ is_continuous = dividend_type - return _crr_init_parameters( - sigma=sigma, - r=r, - T=T, - N=N, - div_yield=div_yield, - is_continuous=is_continuous) - + return _crr_init_parameters(sigma=sigma, r=r, T=T, N=N, div_yield=div_yield, is_continuous=is_continuous) @njit def _crr_init_parameters( - sigma: float, - r: float, - T: float, - N: int, - div_yield: float = 0.0, - is_continuous: bool = True + sigma: float, r: float, T: float, N: int, div_yield: float = 0.0, is_continuous: bool = True ) -> Tuple[float, float, float, float]: """ params: @@ -77,6 +60,7 @@ def _crr_init_parameters( p = (np.exp((r - y) * dt) - d) / (u - d) return u, d, p, dt + @njit def build_tree(S0: float, u: float, d: float, N: int) -> np.ndarray: """ @@ -91,7 +75,7 @@ def build_tree(S0: float, u: float, d: float, N: int) -> np.ndarray: tree = np.zeros((N + 1, N + 1)) for i in range(N + 1): for j in range(i + 1): - tree[i, j] = S0 * (u ** j) * (d ** (i - j)) + tree[i, j] = S0 * (u**j) * (d ** (i - j)) return tree @@ -107,6 +91,7 @@ def apply_discrete_dividends(discrete_dividends: List[Tuple[float, float]], stoc dividends = convert_schedule_to_numba(discrete_dividends) # Convert dividends to a Numba List return _apply_discrete_dividends_jit(dividends, stock_tree, N) + @njit def _apply_discrete_dividends_jit(dividends: List[Tuple[float, float]], tree: np.ndarray, N: int): """ @@ -124,6 +109,7 @@ def _apply_discrete_dividends_jit(dividends: List[Tuple[float, float]], tree: np tree[i, j] = max(tree[i, j] - div, 0) return tree + def create_option_tree(stock_tree: np.ndarray, K: float, option_type: str, N: int) -> np.ndarray: """ Create the option value tree based on the stock price tree. @@ -136,9 +122,10 @@ def create_option_tree(stock_tree: np.ndarray, K: float, option_type: str, N: in """ if stock_tree is None: raise ValueError("stock_tree is None before calling _create_option_tree_jit") - option_type = 0 if option_type.lower() == 'c' else 1 # 0 for call, 1 for put + option_type = 0 if option_type.lower() == "c" else 1 # 0 for call, 1 for put return _create_option_tree_jit(stock_tree, K, option_type, N) + @njit def _create_option_tree_jit(tree: np.ndarray, K: float, option_type: int, N: int) -> np.ndarray: """ @@ -163,7 +150,6 @@ def _create_option_tree_jit(tree: np.ndarray, K: float, option_type: int, N: int return option_tree - def calculate_option_values( stock_tree: np.ndarray, option_values: np.ndarray, @@ -173,7 +159,7 @@ def calculate_option_values( N: int, p: float, american: bool = False, - option_type: int = 0 + option_type: int = 0, ) -> Tuple[float, np.ndarray, np.ndarray]: """ Calculate the option values at each node in the binomial tree. @@ -187,11 +173,8 @@ def calculate_option_values( """ stock_tree = np.asarray(stock_tree) option_values = np.asarray(option_values) - option_type = 0 if option_type.lower() == 'c' else 1 # 0 for call, 1 for put - return _calculate_option_values_jit( - stock_tree, option_values, K, r, dt, N, p, american, option_type - ) - + option_type = 0 if option_type.lower() == "c" else 1 # 0 for call, 1 for put + return _calculate_option_values_jit(stock_tree, option_values, K, r, dt, N, p, american, option_type) @njit @@ -204,7 +187,7 @@ def _calculate_option_values_jit( N: int, p: float, american: bool = False, - option_type: int = 0 + option_type: int = 0, ) -> Tuple[float, np.ndarray, np.ndarray]: """ Calculate the option values at each node in the binomial tree. @@ -220,14 +203,14 @@ def _calculate_option_values_jit( raise ValueError("stock_tree is None before calling _create_option_tree_jit") if option_tree is None: raise ValueError("option_tree is None before calling _calculate_option_values_jit") - + V1 = np.zeros(2) V2 = np.zeros(3) for i in range(N - 1, -1, -1): for j in range(i + 1): expected = np.exp(-r * dt) * (p * option_tree[i + 1, j + 1] + (1 - p) * option_tree[i + 1, j]) if american: - if option_type ==0: + if option_type == 0: intrinsic = max(tree[i, j] - K, 0) else: intrinsic = max(K - tree[i, j], 0) @@ -243,8 +226,6 @@ def _calculate_option_values_jit( return option_tree[0, 0], V1, V2 - - def crr_binomial_pricing( K: float, T: float, @@ -252,15 +233,15 @@ def crr_binomial_pricing( r: float, N: int, S0: float, - option_type: str = 'c', + option_type: str = "c", american: bool = False, div_yield: float = 0.0, - dividends: List[Tuple[float, float]] = [], # noqa - dividend_type: str = 'discrete' + dividends: List[Tuple[float, float]] = [], # noqa + dividend_type: str = "discrete", ) -> float: """ Calculate the price of an option using the Cox-Ross-Rubinstein binomial model. - + Parameters: - K: Strike price - T: Time to expiration (in years) @@ -273,24 +254,20 @@ def crr_binomial_pricing( - div_yield: Dividend yield (annualized, default is 0.0) - dividends: List of tuples (time fraction, amount) for discrete dividends (default is None) - dividend_type: 'discrete' for discrete dividends, 'continuous' for continuous dividends (default is 'discrete') - + If 'discrete', dividends should be a list of tuples where each tuple contains the time fraction (as a float) and the amount (as a float). If 'continuous', div_yield should be provided as a float representing the annualized dividend yield. If no dividends are provided, the function assumes no dividends. If 'dividend_type' is 'continuous', the function will treat the dividend yield as a continuous yield. - + Returns: The calculated price of the option. """ - is_continuous = (dividend_type == 'continuous') - option_type = 0 if option_type.lower() == 'c' else 1 # 0 for call, 1 for put - dividends = convert_schedule_to_numba(dividends) # Convert dividends to a Numba List - - return _crr_binomial_pricing_jit( - K, T, sigma, r, N, S0, option_type, american, - div_yield, dividends, is_continuous - ) + is_continuous = dividend_type == "continuous" + option_type = 0 if option_type.lower() == "c" else 1 # 0 for call, 1 for put + dividends = convert_schedule_to_numba(dividends) # Convert dividends to a Numba List + return _crr_binomial_pricing_jit(K, T, sigma, r, N, S0, option_type, american, div_yield, dividends, is_continuous) @njit @@ -304,12 +281,12 @@ def _crr_binomial_pricing_jit( option_type: int = 0, american: bool = False, div_yield: float = 0.0, - dividends: List[Tuple[float, float]] = [], # noqa - is_continuous: bool = True + dividends: List[Tuple[float, float]] = [], # noqa + is_continuous: bool = True, ) -> float: """ Calculate the price of an option using the Cox-Ross-Rubinstein binomial model. - + Parameters: - K: Strike price - T: Time to expiration (in years) @@ -322,12 +299,12 @@ def _crr_binomial_pricing_jit( - div_yield: Dividend yield (annualized, default is 0.0) - dividends: List of tuples (time fraction, amount) for discrete dividends (default is None) - dividend_type: 'discrete' for discrete dividends, 'continuous' for continuous dividends (default is 'discrete') - + If 'discrete', dividends should be a list of tuples where each tuple contains the time fraction (as a float) and the amount (as a float). If 'continuous', div_yield should be provided as a float representing the annualized dividend yield. If no dividends are provided, the function assumes no dividends. If 'dividend_type' is 'continuous', the function will treat the dividend yield as a continuous yield. - + Returns: The calculated price of the option. """ @@ -339,20 +316,95 @@ def _crr_binomial_pricing_jit( return price +def vector_crr_binomial_pricing( + K: Iterable, + T: Iterable, + sigma: Iterable, + r: Iterable, + N: Iterable, + S0: Iterable, + right: Iterable, + american: Iterable, + dividend_yield: Iterable = None, + dividends: Iterable = None, + dividend_type: Iterable = None, +) -> Iterable[float]: + """ + Vectorized CRR binomial option pricing. + Parameters: + - K: Strike prices + - T: Time to maturities (in years) + - sigma: Volatilities + - r: Risk-free interest rates + - N: Number of binomial steps + - S0: Current underlying asset prices + - right: Option types ('c' for call, 'p' for put) + - american: Flags indicating if options are American (True) or European (False) + - dividend_yield: Continuous dividend yields (optional) + - dividends: Discrete dividend schedules (optional) + - dividend_type: Types of dividends ('continuous' or 'discrete') (optional) + Returns: + - List of option prices + """ + + if dividend_yield is None: + dividend_yield = [0.0] * len(K) + + if dividends is None: + dividends = [()] * len(K) + + if dividend_type is None: + dividend_type = ["continuous"] * len(K) + + assert_equal_length( + K, + T, + sigma, + r, + N, + S0, + right, + american, + dividend_yield, + dividends, + dividend_type, + names=["K", "T", "sigma", "r", "N", "S0", "right", "american", "dividend_yield", "dividends", "dividend_type"], + ) + + return runThreads( + crr_binomial_pricing, + [ + K, # K + T, # T + sigma, # sigma + r, # r + N, # N + S0, # S0 + right, # option_type + american, # american + dividend_yield, # dividend_yield + dividends, # dividends, + dividend_type, + ], + ) + + class BinomialBase(ABC): - def __init__(self, - K: float, - expiration: datetime|str, - sigma: float, - r: float, - N: int = 100, - spot_price: float = None, - dividend_type: str = 'discrete', - div_amount: float = 0.0, - option_type: str = 'c', - start_date: datetime|str = None, - valuation_date: datetime|str = None, - american: bool = False): + def __init__( + self, + K: float, + expiration: datetime | str, + sigma: float, + r: float, + N: int = 100, + spot_price: float = None, + dividend_type: str = "discrete", + div_amount: float = 0.0, + option_type: str = "c", + start_date: datetime | str = None, + valuation_date: datetime | str = None, + american: bool = False, + ): super().__init__() """ Base class for Binomial Tree models. @@ -376,12 +428,12 @@ def __init__(self, self.N = N self.S0 = spot_price self.dividend_type = dividend_type - self.div_yield = div_amount if dividend_type == 'continuous' else 0.0 - self.discrete_dividends = div_amount if dividend_type == 'discrete' else [] + self.div_yield = div_amount if dividend_type == "continuous" else 0.0 + self.discrete_dividends = div_amount if dividend_type == "discrete" else [] self.option_type = option_type self.start_date = start_date self.valuation_date = valuation_date - self.T = time_distance_helper(self.expiration, self.valuation_date or datetime.now()) + self.T = time_distance_helper(end=self.expiration, start=self.valuation_date or datetime.now()) self.american = american self.dt = self.T / self.N self.priced = False @@ -418,9 +470,7 @@ def pricing_warning(self): This method can be overridden in subclasses to provide specific warnings. """ if not self.priced: - # logger.warning("Option has not been priced yet. Please call the price() method first.") - print("Option has not been priced yet. Please call the price() method first.") - + logger.warning("Option has not been priced yet. Please call the price() method first.") def reset_pricing_variables(self): """ @@ -432,7 +482,6 @@ def reset_pricing_variables(self): self.init_parameters() self.build_tree() - def _tree_numerical(self, attr, dx_thresh=0.01): """ Calculate the numerical value of a Greek (delta, gamma, etc.) using the binomial tree. @@ -443,7 +492,7 @@ def _tree_numerical(self, attr, dx_thresh=0.01): bump = actual_value * dx_thresh up_bump = actual_value + bump down_bump = actual_value - bump - + setattr(self, attr, up_bump) price_up = self.price() @@ -454,7 +503,7 @@ def _tree_numerical(self, attr, dx_thresh=0.01): setattr(self, attr, actual_value) return (price_up - price_down) / (2 * bump) - + def _tree_numerical_second_order(self, attr, dx_thresh=0.01): """ Calculate the second-order numerical value of a Greek using the binomial tree. @@ -465,7 +514,7 @@ def _tree_numerical_second_order(self, attr, dx_thresh=0.01): bump = actual_value * dx_thresh up_bump = actual_value + bump down_bump = actual_value - bump - + setattr(self, attr, up_bump) price_up = self.price() @@ -478,8 +527,8 @@ def _tree_numerical_second_order(self, attr, dx_thresh=0.01): ## Reset setattr(self, attr, actual_value) - return (price_up - 2 * price_mid + price_down) / (bump ** 2) - + return (price_up - 2 * price_mid + price_down) / (bump**2) + def theta(self, dx_thresh=0.0001): """ Calculate the theta of the option using the binomial tree. @@ -487,8 +536,8 @@ def theta(self, dx_thresh=0.0001): Returns: Theta value as a float. """ - return -self._tree_numerical('T', dx_thresh)/DAILY_BASIS - + return -self._tree_numerical("T", dx_thresh) / DAILY_BASIS + def vega(self, dx_thresh=0.0001): """ Calculate the vega of the option using the binomial tree. @@ -496,8 +545,8 @@ def vega(self, dx_thresh=0.0001): Returns: Vega value as a float. """ - return self._tree_numerical('sigma', dx_thresh)/100 - + return self._tree_numerical("sigma", dx_thresh) / 100 + def rho(self, dx_thresh=0.0001): """ Calculate the rho of the option using the binomial tree. @@ -505,8 +554,7 @@ def rho(self, dx_thresh=0.0001): Returns: Rho value as a float. """ - return self._tree_numerical('r', dx_thresh)/100 - + return self._tree_numerical("r", dx_thresh) / 100 def volga(self, dx_thresh=0.0001): """ @@ -515,39 +563,47 @@ def volga(self, dx_thresh=0.0001): Returns: Volga value as a float. """ - return self._tree_numerical_second_order('sigma', dx_thresh)/100**2 - - + return self._tree_numerical_second_order("sigma", dx_thresh) / 100**2 def __setattr__(self, name, value): protected = [ - 'K', 'expiration', 'sigma', 'r', 'N', 'S0', 'dividend_type', - 'div_yield', 'discrete_dividends', 'option_type', - 'start_date', 'valuation_date', 'T', 'american' + "K", + "expiration", + "sigma", + "r", + "N", + "S0", + "dividend_type", + "div_yield", + "discrete_dividends", + "option_type", + "start_date", + "valuation_date", + "T", + "american", ] - if not hasattr(self, '_initialized') or not self._initialized: + if not hasattr(self, "_initialized") or not self._initialized: # Allow setting attributes before initialization super().__setattr__(name, value) return - - if hasattr(self, '_initialized') and self._initialized: + + if hasattr(self, "_initialized") and self._initialized: if name in protected: - # raise AttributeError(f"'{name}' is read-only after initialization.") - logger.warning(f"'{name}' is read-only after initialization. Resetting pricing variables.") - super().__setattr__(name, value) ## Set + logger.info(f"'{name}' is read-only after initialization. Resetting pricing variables. This will not change the value of '{name}' but will reset the pricing variables for a new calculation.") + super().__setattr__(name, value) ## Set if name in protected: # Reset pricing variables if a protected attribute is set logger.info(f"Resetting pricing variables due to change in '{name}'.") self.reset_pricing_variables() def __repr__(self): - return f"{self.__class__.__name__}(K={self.K}, expiration={self.expiration}, dividend_type={self.dividend_type})" + return ( + f"{self.__class__.__name__}(K={self.K}, expiration={self.expiration}, dividend_type={self.dividend_type})" + ) - - -class VectorBinomialBase(BinomialBase): +class VectorBinomialBase(BinomialBase): @abstractmethod def init_parameters(self): """ @@ -561,13 +617,8 @@ def build_tree(self): Build the binomial tree structure. This method should be implemented in subclasses. """ - self.stock_tree = build_tree( - S0=self.S0, - u=self.u, - d=self.d, - N=self.N - ) - if self.dividend_type == 'discrete': + self.stock_tree = build_tree(S0=self.S0, u=self.u, d=self.d, N=self.N) + if self.dividend_type == "discrete": self._apply_discrete_dividends() # Apply discrete dividends at time step 0 def _apply_discrete_dividends(self) -> float: @@ -575,19 +626,14 @@ def _apply_discrete_dividends(self) -> float: Apply discrete dividend adjustment to the stock price at a given time step. """ if not list(self.discrete_dividends): - return + return self.stock_tree = apply_discrete_dividends( - discrete_dividends=self.discrete_dividends, - stock_tree=self.stock_tree, - N=self.N + discrete_dividends=self.discrete_dividends, stock_tree=self.stock_tree, N=self.N ) def __create_option_tree(self): self.option_values = create_option_tree( - stock_tree=self.stock_tree, - K=self.K, - option_type=self.option_type, - N=self.N + stock_tree=self.stock_tree, K=self.K, option_type=self.option_type, N=self.N ) def price(self): @@ -602,11 +648,11 @@ def price(self): N=self.N, p=self.p, american=self.american, - option_type=self.option_type + option_type=self.option_type, ) self.priced = True return price - + def delta(self): """ Calculate the delta of the option using the binomial tree. @@ -617,14 +663,14 @@ def delta(self): self.pricing_warning() if self.N < 1: raise ValueError("N must be at least 1 to calculate delta.") - - if not hasattr(self, 'V1'): + + if not hasattr(self, "V1"): self.price() - + stock_tree = self.stock_tree delta = (self.V1[1] - self.V1[0]) / (stock_tree[1][1] - stock_tree[1][0]) return delta - + def gamma(self): """ Calculate the gamma of the option using the binomial tree. @@ -635,65 +681,56 @@ def gamma(self): self.pricing_warning() if self.N < 2: raise ValueError("N must be at least 2 to calculate gamma.") - - if not hasattr(self, 'V2'): + + if not hasattr(self, "V2"): self.price() - + V2, S2 = self.V2, self.stock_tree[2] delta_up = (V2[2] - V2[1]) / (S2[2] - S2[1]) delta_down = (V2[1] - V2[0]) / (S2[1] - S2[0]) gamma = (delta_up - delta_down) / ((S2[2] - S2[0]) / 2) # Average change in delta over the interval return gamma - + ## Child classes for specific binomial models (Vector Operators) -class VectorBinomialCRR(VectorBinomialBase): +class VectorBinomialCRR(VectorBinomialBase): def init_parameters(self): """ Initialize parameters for the binomial tree. This method should be called before building the tree. """ - q = self.div_yield if self.dividend_type == 'continuous' else 0.0 + q = self.div_yield if self.dividend_type == "continuous" else 0.0 self.u, self.d, self.p, _ = crr_init_parameters( - sigma=self.sigma, - r=self.r, - T=self.T, - N=self.N, - div_yield=q, - dividend_type=self.dividend_type + sigma=self.sigma, r=self.r, T=self.T, N=self.N, div_yield=q, dividend_type=self.dividend_type ) + class VectorBinomialLR(VectorBinomialBase): # or NodeBinomialBase def init_parameters(self): """ Initialize Leisen-Reimer parameters: u, d, p. """ - q = self.div_yield if self.dividend_type == 'continuous' else 0.0 + q = self.div_yield if self.dividend_type == "continuous" else 0.0 self.dt = self.T / self.N v = self.sigma * np.sqrt(self.dt) self.u = np.exp(v) self.d = np.exp(-v) - d1 = ( - np.log(self.S0 / self.K) + - (self.r - q + 0.5 * self.sigma ** 2) * self.T - ) / (self.sigma * np.sqrt(self.T)) + d1 = (np.log(self.S0 / self.K) + (self.r - q + 0.5 * self.sigma**2) * self.T) / (self.sigma * np.sqrt(self.T)) x = d1 # Can also use d2 for puts, but d1 gives better results overall # Peizer-Pratt inversion of CDF (used by Leisen-Reimer) - w = np.sqrt(1 - np.exp(-2 * (x ** 2) / self.N)) + w = np.sqrt(1 - np.exp(-2 * (x**2) / self.N)) self.p = 0.5 + np.sign(x) * w / 2 - - # Child classes for specific binomial models (Node Operators) class Node(Scalar): - def __init__(self, stock_price, position,option_value=0.0): + def __init__(self, stock_price, position, option_value=0.0): super().__init__(value=option_value) self.stock_price = stock_price self.value = option_value @@ -704,7 +741,7 @@ def __init__(self, stock_price, position,option_value=0.0): @property def option_value(self): return self.value - + @option_value.setter def option_value(self, value): self.value = value @@ -713,15 +750,12 @@ def __eq__(self, value): if isinstance(value, Node): return self.stock_price == value.stock_price and self.position == value.position return False - + def __repr__(self): return f"Node(price={self.stock_price}, option_value={self.option_value}, pos={self.position})" - - class NodeBinomialBase(BinomialBase): - @abstractmethod def init_parameters(self): """ @@ -739,7 +773,7 @@ def build_tree(self): for i in range(self.N + 1): level = [] for j in range(i + 1): - S = self.S0 * (self.u ** j) * (self.d ** (i - j)) + S = self.S0 * (self.u**j) * (self.d ** (i - j)) node = Node(stock_price=S, position=(i, j)) level.append(node) self.tree.append(level) @@ -747,11 +781,10 @@ def build_tree(self): for i in range(self.N): for j in range(i + 1): current = self.tree[i][j] - current.down = self.tree[i+1][j] # one down move - current.up = self.tree[i+1][j+1] # one up move - + current.down = self.tree[i + 1][j] # one down move + current.up = self.tree[i + 1][j + 1] # one up move - if self.dividend_type == 'discrete': + if self.dividend_type == "discrete": self._apply_discrete_dividends() # Apply discrete dividends at time step 0 def _apply_discrete_dividends(self) -> float: @@ -759,22 +792,19 @@ def _apply_discrete_dividends(self) -> float: Apply discrete dividend adjustment to the stock price at a given time step. """ if not list(self.discrete_dividends): - return - + return + for t_frac, div in self.discrete_dividends: div_step = min(int(round(t_frac * self.N)), self.N) for i in range(div_step, self.N + 1): for node in self.tree[i]: node.stock_price = max(node.stock_price - div, 0) - def __create_option_tree(self): terminal_nodes = self.tree[-1] for node in terminal_nodes: node.option_value = ( - max(0, node.stock_price - self.K) - if self.option_type == 'c' - else max(0, self.K - node.stock_price) + max(0, node.stock_price - self.K) if self.option_type == "c" else max(0, self.K - node.stock_price) ) def price(self): @@ -785,14 +815,12 @@ def price(self): for _, node in enumerate(tree[i]): up_val = node.up.option_value down_val = node.down.option_value - expected = np.exp(-self.r * self.dt) * ( - self.p * up_val + (1 - self.p) * down_val - ) + expected = np.exp(-self.r * self.dt) * (self.p * up_val + (1 - self.p) * down_val) if self.american: intrinsic = ( max(node.stock_price - self.K, 0) - if self.option_type == 'c' + if self.option_type == "c" else max(self.K - node.stock_price, 0) ) node.option_value = max(expected, intrinsic) @@ -801,8 +829,7 @@ def price(self): self.priced = True return tree[0][0].option_value - - + def delta(self): """ Calculate the delta of the option using the binomial tree. @@ -813,12 +840,12 @@ def delta(self): self.pricing_warning() if self.N < 1: raise ValueError("N must be at least 1 to calculate delta.") - + stock_tree = self.tree V1 = self.tree[1] delta = (V1[1] - V1[0]) / (stock_tree[1][1].stock_price - stock_tree[1][0].stock_price) return delta - + def gamma(self): """ Calculate the gamma of the option using the binomial tree. @@ -829,73 +856,71 @@ def gamma(self): self.pricing_warning() if self.N < 2: raise ValueError("N must be at least 2 to calculate gamma.") - - if not hasattr(self, 'V2'): + + if not hasattr(self, "V2"): self.price() - + V2, S2 = self.tree[2], self.tree[2] delta_up = (V2[2] - V2[1]) / (S2[2].stock_price - S2[1].stock_price) delta_down = (V2[1] - V2[0]) / (S2[1].stock_price - S2[0].stock_price) - gamma = (delta_up - delta_down) / ((S2[2].stock_price - S2[0].stock_price) / 2) # Average change in delta over the interval + gamma = (delta_up - delta_down) / ( + (S2[2].stock_price - S2[0].stock_price) / 2 + ) # Average change in delta over the interval return gamma - class NodeBinomialCRR(NodeBinomialBase): - def init_parameters(self): """ Initialize parameters for the binomial tree. This method should be called before building the tree. """ - if self.dividend_type == 'continuous': - y = self.div_yield ## Continuous dividend yield adjustment + if self.dividend_type == "continuous": + y = self.div_yield ## Continuous dividend yield adjustment else: y = 0.0 self.u = np.exp(self.sigma * np.sqrt(self.dt)) self.d = np.exp(-(self.sigma * np.sqrt(self.dt))) self.p = (np.exp((self.r - y) * self.dt) - self.d) / (self.u - self.d) - class NodeBinomialLR(NodeBinomialBase): # or NodeBinomialBase def init_parameters(self): """ Initialize Leisen-Reimer parameters: u, d, p. """ - q = self.div_yield if self.dividend_type == 'continuous' else 0.0 + q = self.div_yield if self.dividend_type == "continuous" else 0.0 self.dt = self.T / self.N v = self.sigma * np.sqrt(self.dt) self.u = np.exp(v) self.d = np.exp(-v) - d1 = ( - np.log(self.S0 / self.K) + - (self.r - q + 0.5 * self.sigma ** 2) * self.T - ) / (self.sigma * np.sqrt(self.T)) + d1 = (np.log(self.S0 / self.K) + (self.r - q + 0.5 * self.sigma**2) * self.T) / (self.sigma * np.sqrt(self.T)) x = d1 # Can also use d2 for puts, but d1 gives better results overall # Peizer-Pratt inversion of CDF (used by Leisen-Reimer) - w = np.sqrt(1 - np.exp(-2 * (x ** 2) / self.N)) + w = np.sqrt(1 - np.exp(-2 * (x**2) / self.N)) self.p = 0.5 + np.sign(x) * w / 2 # Market Child Classes class MarketBinomial(VectorBinomialCRR): - def __init__(self, - tick: str, - K: float, - expiration: datetime|str, - sigma: float, - N: int = 100, - dividend_type: str = 'discrete', - option_type: str = 'c', - start_date: datetime|str = None, - valuation_date: datetime|str = None, - r: float = None, - american: bool = False): + def __init__( + self, + tick: str, + K: float, + expiration: datetime | str, + sigma: float, + N: int = 100, + dividend_type: str = "discrete", + option_type: str = "c", + start_date: datetime | str = None, + valuation_date: datetime | str = None, + r: float = None, + american: bool = False, + ): # super().__init__() """ Base class for Binomial Tree models. @@ -923,14 +948,14 @@ def __init__(self, valuation_date=valuation_date or datetime.now(), risk_free_rate=r, dividend_type=dividend_type, - dividend=None # Market dividend will be set later + dividend=None, # Market dividend will be set later ) self.r = r or self.forward.risk_free_rate self.dividend_type = dividend_type self.option_type = option_type self.start_date = start_date self.valuation_date = valuation_date - self.T = time_distance_helper(self.expiration, self.valuation_date or datetime.now()) + self.T = time_distance_helper(end=self.expiration, start=self.valuation_date or datetime.now()) self.american = american self.dt = self.T / self.N self.tree = [] @@ -963,7 +988,7 @@ def discrete_dividends(self): return self.forward.dividend.get_year_fractions() else: return [] - + @property def div_yield(self): """ @@ -973,6 +998,3 @@ def div_yield(self): return self.forward.dividend.yield_rate else: return 0.0 - - - \ No newline at end of file diff --git a/trade/optionlib/pricing/black_scholes.py b/trade/optionlib/pricing/black_scholes.py index da952a0..f89e4b2 100644 --- a/trade/optionlib/pricing/black_scholes.py +++ b/trade/optionlib/pricing/black_scholes.py @@ -25,6 +25,7 @@ vectorized_market_forward_calc ) from trade.helpers.Logging import setup_logger +from trade.optionlib.utils.format import assert_equal_length logger = setup_logger('trade.optionlib.pricing.black_scholes') @@ -53,6 +54,7 @@ def black_scholes_vectorized(F: Union[float, np.ndarray], r = convert_to_array_individual(r, dtype=float) sigma = convert_to_array_individual(sigma, dtype=float) option_type = convert_to_array_individual(option_type, dtype=str) + assert_equal_length(F, K, T, r, sigma, option_type, names=['F', 'K', 'T', 'r', 'sigma', 'option_type']) d1 = (np.log(F / K) + 0.5 * sigma**2 * T) / (sigma * np.sqrt(T)) @@ -143,7 +145,7 @@ def black_scholes_vectorized_market(ticks: List[str], raise ValueError("option_type must be a single string or a list of strings with the same length as ticks.") # Convert valuation_dates and end_dates to Timedelta - T = [time_distance_helper(end_dates[i], valuation_dates[i]) for i in range(len(end_dates))] + T = [time_distance_helper(end=end_dates[i], start=valuation_dates[i]) for i in range(len(end_dates))] return black_scholes_vectorized_base( F=F, @@ -183,7 +185,7 @@ def __init__(self, - volatility: sigma (annualized) - option_type: "call" or "put" """ - self.T = time_distance_helper(expiration, valuation_date) + self.T = time_distance_helper(end=expiration, start=valuation_date) risk_free_rate = float(risk_free_rate) if risk_free_rate else 0 # Ensure risk-free rate is a float option_inputs_assert(sigma=volatility, K=strike_price, @@ -452,7 +454,7 @@ def price(self, F=None, K=None, T=None, r=None, sigma=None, option_type=None, S= else: # Set valuation date back so that (end_date - valuation_date) = T - temp_val_date = self.forward.valuation_date + _ = self.forward.valuation_date new_val_date = self.expiration - timedelta(days=T * DAILY_BASIS) self.forward.valuation_date = new_val_date self.forward.dividend.valuation_date = new_val_date diff --git a/trade/optionlib/utils/batch_operation.py b/trade/optionlib/utils/batch_operation.py index 66c9a4c..3f4d031 100644 --- a/trade/optionlib/utils/batch_operation.py +++ b/trade/optionlib/utils/batch_operation.py @@ -2,9 +2,8 @@ import numpy as np from itertools import chain from trade.helpers.helper import get_parrallel_apply -from typing import List, Union -parrallel_apply = get_parrallel_apply() ## Using system to pick btwmeen multiprocessing and threading + def vector_batch_processor(callable, *args, **kwargs): """ @@ -50,7 +49,9 @@ def vector_batch_processor(callable, *args, **kwargs): else: split_arg = [arg] * num_process ordered_inputs.append(split_arg) - + + parrallel_apply = get_parrallel_apply() ## Using system to pick btwmeen multiprocessing and threading + results = parrallel_apply( func=callable, OrderedInputs=ordered_inputs, diff --git a/trade/optionlib/utils/format.py b/trade/optionlib/utils/format.py index 69f4d30..d3388f7 100644 --- a/trade/optionlib/utils/format.py +++ b/trade/optionlib/utils/format.py @@ -2,17 +2,24 @@ import numpy as np import pandas as pd from trade.helpers.Logging import setup_logger -logger = setup_logger('trade.optionlib.utils.format') -def assert_equal_length(*args): +logger = setup_logger("trade.optionlib.utils.format") + + +def assert_equal_length(*args, names: list = None): """ Assert that all input lists have the same length. """ lengths = [len(arg) for arg in args] if len(set(lengths)) != 1: - return False + if names is not None: + name_length_pairs = ", ".join(f"{name}: {length}" for name, length in zip(names, lengths)) + raise ValueError(f"Input lists must have the same length. Lengths are: {name_length_pairs}") + else: + raise ValueError(f"Input lists must have the same length. Lengths are: {lengths}") return True + def convert_to_array_individual(value, dtype=None): if isinstance(value, (list, np.ndarray, pd.Series)): return np.array(value, dtype=dtype or object) @@ -31,8 +38,6 @@ def convert_to_array(*args): def option_inputs_assert(sigma, K, S0, T, r, q, market_price, flag): - - """ Check for errors in the input parameters for the vol backout function Args: @@ -44,14 +49,16 @@ def option_inputs_assert(sigma, K, S0, T, r, q, market_price, flag): q (float): Dividend yield. market_price (float): Market price of the option. flag (str): 'c' for call option, 'p' for put option. - + """ assert isinstance(sigma, (int, float)), f"Recieved '{type(sigma)}' for sigma. Expected 'int' or 'float'" assert isinstance(K, (int, float)), f"Recieved '{type(K)}' for K. Expected 'int' or 'float'" assert isinstance(S0, (int, float)), f"Recieved '{type(S0)}' for S0. Expected 'int' or 'float'" assert isinstance(r, (int, float)), f"Recieved '{type(r)}' for r. Expected 'int' or 'float'" assert isinstance(q, (int, float)), f"Recieved '{type(q)}' for q. Expected 'int' or 'float'" - assert isinstance(market_price, (int, float)), f"Recieved '{type(market_price)}' for market_price. Expected 'int' or 'float'" + assert isinstance( + market_price, (int, float) + ), f"Recieved '{type(market_price)}' for market_price. Expected 'int' or 'float'" assert isinstance(flag, str), f"Recieved '{type(flag)}' for flag. Expected 'str'" if sigma <= 0: @@ -68,13 +75,11 @@ def option_inputs_assert(sigma, K, S0, T, r, q, market_price, flag): raise ValueError("Dividend yield must be non-negative.") if market_price <= 0: raise ValueError("Market price must be positive.") - if flag not in ['c', 'p']: + if flag not in ["c", "p"]: raise ValueError("Flag must be 'c' for call or 'p' for put.") - + if pd.isna(sigma) or pd.isna(K) or pd.isna(S0) or pd.isna(r) or pd.isna(q) or pd.isna(market_price): raise ValueError("Input values cannot be NaN.") - - def to_1d_array(x): @@ -83,6 +88,7 @@ def to_1d_array(x): return x.flatten() return x + def equalize_lengths(*args): """ Ensure all inputs have the same length if an arg is a list of size 1 or a single value. @@ -91,5 +97,7 @@ def equalize_lengths(*args): if max_size == 0: raise ValueError("All input arrays are empty.") - return [np.full(max_size, arg) if isinstance(arg, (int, float, str, datetime)) or len(arg) == 1 else np.asarray(arg) for arg in args] - + return [ + np.full(max_size, arg) if isinstance(arg, (int, float, str, datetime)) or len(arg) == 1 else np.asarray(arg) + for arg in args + ] diff --git a/trade/optionlib/utils/market_data.py b/trade/optionlib/utils/market_data.py index a2bb9c0..e1cbcfb 100644 --- a/trade/optionlib/utils/market_data.py +++ b/trade/optionlib/utils/market_data.py @@ -11,6 +11,8 @@ DIVIDEND_CACHE = {} + + def get_div_schedule(ticker, filter_specials=True): """ Fetch the dividend schedule for a given ticker. diff --git a/trade/optionlib/vol/implied_vol.py b/trade/optionlib/vol/implied_vol.py index 8569ac8..2391d35 100644 --- a/trade/optionlib/vol/implied_vol.py +++ b/trade/optionlib/vol/implied_vol.py @@ -1,51 +1,331 @@ -from typing import List, Union, Literal +from typing import Any, List, Union, Literal, Callable, Optional import numpy as np +import pandas as pd +import time from scipy.optimize import minimize, minimize_scalar -from functools import lru_cache +from trade.optionlib.utils.format import assert_equal_length # noqa +from trade.optionlib.utils.batch_operation import vector_batch_processor +from trade import set_pool_enabled, get_pool_enabled from ..pricing.black_scholes import black_scholes_vectorized, black_scholes_vectorized_scalar from ..pricing.bjs2002 import bjerksund_stensland_2002_vectorized from ..pricing.binomial import crr_binomial_pricing from ..config.defaults import BRUTE_FORCE_MAX_ITERATIONS from trade.helpers.Logging import setup_logger -logger = setup_logger('trade.optionlib.vol.implied_vol') +from trade.helpers.exit_helpers import _record_time +import random +logger = setup_logger("trade.optionlib.vol.implied_vol") -def intrinsic_check(F, K, T, r, sigma, market_price, option_type) -> None: + +def vector_crr_iv_estimation( + S: List[float], + K: List[float], + T: List[float], + r: List[float], + market_price: List[float], + dividends: List[Any], + option_type: List[str], + N: List[int] = None, + dividend_type: List[str] = None, + american: List[bool] = None, +) -> List[float]: + """Estimate implied volatilities using Cox-Ross-Rubinstein binomial model for multiple options. + + Vectorized implementation that computes implied volatilities by matching market prices + to CRR binomial tree prices. Automatically selects between standard and batch processing + based on input size (threshold: 200 options). Supports both American and European options + with discrete or continuous dividend treatments. + + Args: + S: List of spot prices for each option. + K: List of strike prices for each option. + T: List of times to maturity (in years) for each option. + r: List of risk-free interest rates (annualized) for each option. + market_price: List of observed market prices to match. + dividends: List of dividend inputs. Format depends on dividend_type: + - For "discrete": Schedule objects or tuples of (ex_date, amount) + - For "continuous": continuous dividend yields (floats) + option_type: List of option types ('c' for call, 'p' for put). + N: Number of time steps in binomial tree for each option. Defaults to 100 for all. + dividend_type: List of dividend types ('discrete' or 'continuous'). + Defaults to 'discrete' for all. + american: List of booleans indicating American (True) or European (False) exercise. + Defaults to True (American) for all. + + Returns: + List of estimated implied volatilities, one per input option. Returns None for + options where optimization fails to converge. + + Raises: + ValueError: If input lists have inconsistent lengths (via assert_equal_length). + + Examples: + >>> # Basic usage with European calls + >>> spots = [100.0, 105.0, 110.0] + >>> strikes = [100.0, 100.0, 100.0] + >>> maturities = [0.25, 0.25, 0.25] + >>> rates = [0.05, 0.05, 0.05] + >>> prices = [5.2, 7.8, 11.3] + >>> divs = [0.02, 0.02, 0.02] + >>> types = ['c', 'c', 'c'] + >>> + >>> ivs = vector_crr_iv_estimation( + ... S=spots, + ... K=strikes, + ... T=maturities, + ... r=rates, + ... market_price=prices, + ... dividends=divs, + ... option_type=types, + ... N=[100, 100, 100], + ... dividend_type=['continuous', 'continuous', 'continuous'], + ... american=[False, False, False] + ... ) + >>> print(ivs) + [0.234, 0.241, 0.248] + + >>> # American options with discrete dividends (using defaults) + >>> from trade.optionlib.utils.schedule import Schedule + >>> div_schedule = Schedule([("2026-03-15", 0.50), ("2026-06-15", 0.50)]) + >>> ivs = vector_crr_iv_estimation( + ... S=[150.0, 155.0], + ... K=[150.0, 150.0], + ... T=[0.5, 0.5], + ... r=[0.04, 0.04], + ... market_price=[8.5, 10.2], + ... dividends=[div_schedule, div_schedule], + ... option_type=['p', 'p'] + ... ) # Uses defaults: N=100, dividend_type='discrete', american=True """ - Check if the intrinsic value of the option is greater than the market price. - If not, log a warning and return NaN. - Parameters: - - F: Forward price - - K: Strike price - - T: Time to maturity - - r: Risk-free rate - - sigma: Volatility - - market_price: Market price of the option - - option_type: 'c' for call, 'p' for put + randint = random.randint(1,3) + if not american: + american = [True] * len(S) + + if not dividend_type: + dividend_type = ["discrete"] * len(S) + + if not N: + N = [100] * len(S) + + assert_equal_length( + S, + K, + T, + r, + market_price, + dividends, + option_type, + N, + dividend_type, + american, + names=[ + "S", + "K", + "T", + "r", + "market_price", + "dividends", + "option_type", + "N", + "dividend_type", + "american", + ], + ) + if randint == 1: + logger.info("Using non-batch processor for CRR implied volatility estimation.") + start = time.time() + result = vector_vol_estimation( + estimate_crr_implied_volatility, + S, + K, + T, + r, + market_price, + dividends, + option_type, + N, + dividend_type, + american, + ) + _record_time(start, + time.time(), + "crr_iv_estimation", + { + "method": "non-batch crr iv estimation", + "nsize": len(S), + "randint": randint + } + ) + + else: + logger.info("Using batch processor for CRR implied volatility estimation.") + current_pool_status = get_pool_enabled() + additional_info = "Batch CRR With " + if randint == 3: + set_pool_enabled(False) ## Use Threading + additional_info += "Threading." + elif randint ==2: + set_pool_enabled(True) ## Use Multiprocessing + additional_info += "Multiprocessing." + start = time.time() + result = vector_batch_processor( + vector_vol_estimation, + estimate_crr_implied_volatility, + S, + K, + T, + r, + market_price, + dividends, + option_type, + N, + dividend_type, + american, + ) + _record_time(start, time.time(), + "crr_iv_estimation", + { + "method": additional_info, + "nsize": len(S), + "randint": randint + } + ) + set_pool_enabled(current_pool_status) ## Reset to original state + return result + + +def vector_bsm_iv_estimation( + F: List[float], + K: List[float], + T: List[float], + r: List[float], + market_price: List[float], + right: List[str], +) -> List[float]: + """Estimate implied volatilities using Black-Scholes-Merton model for multiple European options. + + Vectorized implementation that computes implied volatilities by matching market prices + to Black-Scholes-Merton prices using a brute force grid search method. This function + is optimized for European-style options and uses forward prices (F) directly rather + than spot prices with dividend adjustments. + + The brute force approach tests a range of volatilities (0.001 to 5.0) and selects the + one that minimizes the difference between calculated and market prices. Returns NaN for + options that violate no-arbitrage bounds (intrinsic value or upper bound constraints). + + Args: + F: List of forward prices for each option. Forward price should already incorporate + dividends and cost of carry: F = S * exp((r-q)*T). + K: List of strike prices for each option. + T: List of times to maturity (in years) for each option. + r: List of risk-free interest rates (annualized) for each option. + market_price: List of observed market prices to match. + right: List of option types ('c' for call, 'p' for put). + Returns: - - None + List of estimated implied volatilities, one per input option. Returns np.nan for + options where arbitrage bounds are violated. + + Raises: + ValueError: If input lists have inconsistent lengths (via assert_equal_length). + + Examples: + >>> # Basic usage with European call options + >>> forwards = [102.5, 107.3, 112.8] + >>> strikes = [100.0, 100.0, 100.0] + >>> maturities = [0.25, 0.25, 0.25] + >>> rates = [0.05, 0.05, 0.05] + >>> prices = [5.8, 9.2, 13.5] + >>> types = ['c', 'c', 'c'] + >>> + >>> ivs = vector_bsm_iv_estimation( + ... F=forwards, + ... K=strikes, + ... T=maturities, + ... r=rates, + ... market_price=prices, + ... right=types + ... ) + >>> print(ivs) + [0.235, 0.242, 0.251] + + >>> # Mixed calls and puts with varying parameters + >>> ivs = vector_bsm_iv_estimation( + ... F=[100.0, 105.0, 98.0], + ... K=[100.0, 110.0, 100.0], + ... T=[0.5, 0.75, 0.25], + ... r=[0.04, 0.045, 0.035], + ... market_price=[8.5, 7.2, 3.1], + ... right=['c', 'c', 'p'] + ... ) + """ + + assert_equal_length( + F, + K, + T, + r, + market_price, + right, + names=[ + "F", + "K", + "T", + "r", + "market_price", + "right", + ], + ) + return vector_vol_estimation(bsm_vol_est_brute_force, F, K, T, r, market_price, right) + + +def intrinsic_check(F, K, T, r, sigma, market_price, option_type) -> bool: + """ + Check no-arbitrage bounds (intrinsic + upper bound). + Returns False if violated, True otherwise. """ df = np.exp(-r * T) - intrinsic_value = df * max(F - K if option_type == 'c' else K - F, 0) - ##TODO: Take this out of objective function to avoid repeated logging during minimization - if intrinsic_value < market_price: - logger.warning("Market price exceeds intrinsic value, returning NaN.") - logger.warning(f"Intrinsic Value: {intrinsic_value}, Market Price: {market_price}. Option Details: F={F}, K={K}, T={T}, r={r}, sigma={sigma}, option_type={option_type}") + if option_type == "c": + intrinsic_value = df * max(F - K, 0.0) + upper_bound = df * F + else: + intrinsic_value = df * max(K - F, 0.0) + upper_bound = df * K + + # Lower bound (intrinsic) violation + if market_price < intrinsic_value: + logger.warning("Market price below intrinsic value.") + logger.warning( + f"Intrinsic Value: {intrinsic_value}, Market Price: {market_price}. " + f"Option Details: F={F}, K={K}, T={T}, r={r}, sigma={sigma}, option_type={option_type}" + ) + return False + + # Upper bound (no-arbitrage) violation + if market_price > upper_bound: + logger.warning("Market price exceeds no-arbitrage upper bound.") + logger.warning( + f"Upper Bound: {upper_bound}, Market Price: {market_price}. " + f"Option Details: F={F}, K={K}, T={T}, r={r}, sigma={sigma}, option_type={option_type}" + ) + return False + + return True def bsm_vol_est_minimization( - F: float, - K: float, - T: float, - r: float, - market_price: float, - option_type: str = 'c', + F: float, + K: float, + T: float, + r: float, + market_price: float, + option_type: str = "c", ): """ Objective function for volatility estimation using minimization. This function calculates the difference between the market price and the Black-Scholes price. - + Parameters: - F: Forward price - K: Strike price @@ -53,21 +333,14 @@ def bsm_vol_est_minimization( - r: Risk-free rate - market_price: Market price of the option - option_type: 'c' for call, 'p' for put - + Returns: - Difference between market price and Black-Scholes price """ intrinsic_check(F, K, T, r, 0.2, market_price, option_type) # Check intrinsic value - + def objective_function(sigma): - bs_price = black_scholes_vectorized( - F=F, - K=K, - T=T, - r=r, - sigma=sigma, - option_type=option_type - ) + bs_price = black_scholes_vectorized(F=F, K=K, T=T, r=r, sigma=sigma, option_type=option_type) return (bs_price - market_price) ** 2 # Initial guess for volatility @@ -75,19 +348,20 @@ def objective_function(sigma): # Minimize the objective function to find the implied volatility result = minimize(objective_function, initial_guess, bounds=[(0.01, None)]) - + if result.success: return result.x[0] # Return the estimated volatility else: raise ValueError("Volatility estimation failed.") - + + def bsm_vol_est_brute_force( - F: float, - K: float, - T: float, - r: float, - market_price: float, - option_type: str = 'c', + F: float, + K: float, + T: float, + r: float, + market_price: float, + option_type: str = "c", ): """ @@ -103,17 +377,13 @@ def bsm_vol_est_brute_force( Returns: - Estimated volatility """ - intrinsic_check(F, K, T, r, 0.2, market_price, option_type) # Check intrinsic value + + check = intrinsic_check(F, K, T, r, 0.2, market_price, option_type) # Check intrinsic value + if not check: + return np.nan sigmas = np.linspace(0.001, 5, BRUTE_FORCE_MAX_ITERATIONS) # Range of volatilities to test - prices = black_scholes_vectorized_scalar( - F=F, - K=K, - T=T, - r=r, - sigma=sigmas, - option_type=option_type - ) + prices = black_scholes_vectorized_scalar(F=F, K=K, T=T, r=r, sigma=sigmas, option_type=option_type) # Calculate the absolute differences between market price and calculated prices differences = np.abs(prices - market_price) @@ -124,52 +394,83 @@ def bsm_vol_est_brute_force( return sigmas[min_index] # Return the estimated volatility and corresponding price -def vector_vol_estimation(brute_callable: Union[callable, str], - list_input: List[tuple], - *args) -> List[float]: - """ - Wrapper function to allow passing a callable and a list of inputs, in order to replicate vectorized behavior.. - This function works by using list comprehension to apply the callable to each set of parameters in the list_input. - - Parameters: - - brute_callable: Function to call for brute force estimation - - list_input: List of inputs for the brute force estimation - eg: [ - (S1, K1, T1, r1, market_price1, q1, option_type1), - ] - +def vector_vol_estimation( + brute_callable: Union[Callable, str], *args, list_input: Optional[List[tuple]] = None +) -> List[float]: + """Vectorized volatility estimation using list comprehension. + + Wrapper function to replicate vectorized behavior by applying a callable to each + set of parameters. Supports two input modes: individual parameter lists (*args) + or pre-zipped tuples (list_input keyword argument). + + Args: + brute_callable: Function to call for volatility estimation. Should accept + parameters matching those in list_input or *args. + *args: Individual parameter lists as separate arguments. Each argument should + be a list/tuple/array of values. These will be transposed into tuples. + list_input: Optional keyword-only. List of tuples where each tuple contains + all parameters for one estimation call. + Example: [(S1, K1, T1, r1, price1, q1, type1), (S2, K2, T2, ...)] + Returns: - - Estimated volatilities as a numpy array + List of estimated volatilities, one per parameter set. + + Raises: + ValueError: If both list_input and *args are provided. + ValueError: If *args elements are not lists, tuples, or arrays. + + Examples: + >>> # Using *args (recommended for most cases) + >>> vols = vector_vol_estimation( + ... bsm_vol_est_brute_force, + ... S_list, K_list, T_list, r_list, market_price_list, option_type_list + ... ) + + >>> # Using list_input keyword argument + >>> vols = vector_vol_estimation( + ... bsm_vol_est_brute_force, + ... list_input=[ + ... (100.0, 100.0, 1.0, 0.05, 10.5, 'c'), + ... (105.0, 100.0, 1.0, 0.05, 12.3, 'c'), + ... ] + ... ) + + Notes: + - Cannot use both *args and list_input simultaneously + - When using *args, all lists must have the same length + - Empty inputs return empty list """ ## Can either pass list_input or args, but not both is_list_input = list_input is not None is_args = len(args) > 0 if is_list_input and is_args: - raise ValueError("Either provide list_input or args, not both. If passing list_input, it should be a list of tuples with all parameters. Pass None for list_input if using args.") - + raise ValueError("Either provide list_input (keyword-only) or *args, not both.") + if args: for arg in args: - if not isinstance(arg,(list, tuple, np.ndarray)): - raise ValueError("args must be a list, tuple, or numpy array.") + if not isinstance(arg, (list, tuple, np.ndarray)): + if isinstance(arg, pd.Series): + arg = arg.tolist() + continue + raise ValueError(f"args must be a list, tuple, or numpy array. Recieved {type(arg)}.") list_input = list(zip(*args)) # Transpose args to create list of tuples if len(list_input) == 0: return [] estimated_vols = [brute_callable(*params) for params in list_input] - return estimated_vols def vol_est_brute_force_bjs_2002( - S: float, - K: float, - T: float, - r: float, - market_price: float, - q: float = 0.0, - option_type: str = 'c', + S: float, + K: float, + T: float, + r: float, + market_price: float, + q: float = 0.0, + option_type: str = "c", ): """ @@ -186,8 +487,8 @@ def vol_est_brute_force_bjs_2002( Returns: - Estimated volatility """ - # - + # + sigmas = np.linspace(0.001, 5, BRUTE_FORCE_MAX_ITERATIONS) # Range of volatilities to test S, K, T, r, q, option_type = map(np.asarray, (S, K, T, r, q, option_type)) prices = bjerksund_stensland_2002_vectorized( @@ -197,8 +498,8 @@ def vol_est_brute_force_bjs_2002( r=r, sigma=sigmas, option_type=option_type, - dividend_type='continuous', # Assuming continuous dividends for this example - dividend=q # No discrete dividends in this case + dividend_type="continuous", # Assuming continuous dividends for this example + dividend=q, # No discrete dividends in this case ) non_na_mask = ~np.isnan(prices) & ~np.isinf(prices) # Filter out NaN/Inf prices prices = prices[non_na_mask] # Filter prices @@ -212,34 +513,36 @@ def vol_est_brute_force_bjs_2002( # Return the corresponding volatility return sigmas[min_index] # Return the estimated volatility and corresponding price + def _k(x, nd=4): """ Helper function to round a number to a specified number of decimal places. Parameters: - x: Number to round - nd: Number of decimal places (default is 4) - + Returns: - Rounded number """ return round(x, nd) -@lru_cache(maxsize=2048) + +# @lru_cache(maxsize=2048) def _estimate_crr_cached( - S: float, - K: float, - T: float, - r: float, - market_price: float, - q: float = 0.0, - option_type: str = 'c', - N: int = 1000, - dividend_type: Literal['continuous', 'discrete'] = 'continuous', - american: bool = False + S: float, + K: float, + T: float, + r: float, + market_price: float, + q: float = 0.0, + option_type: str = "c", + N: int = 1000, + dividend_type: Literal["continuous", "discrete"] = "continuous", + american: bool = False, ) -> float: """ Estimate implied volatility using optimization. - + Parameters: - S: Spot price - K: Strike price @@ -249,10 +552,11 @@ def _estimate_crr_cached( - q: Continuous dividend yield (default is 0.0) - option_type: 'c' for call, 'p' for put - N: Number of time steps in the binomial tree - + Returns: - Estimated volatility """ + def binomial_objective_function(sigma: float) -> float: calculated_price = crr_binomial_pricing( K=K, @@ -262,37 +566,37 @@ def binomial_objective_function(sigma: float) -> float: N=N, S0=S, dividend_type=dividend_type, - div_yield=q if dividend_type == 'continuous' else 0.0, # Use q for continuous dividends - dividends=q if dividend_type == 'discrete' else [], # Use q for discrete dividends + div_yield=q if dividend_type == "continuous" else 0.0, # Use q for continuous dividends + dividends=q if dividend_type == "discrete" else [], # Use q for discrete dividends option_type=option_type, - american=american + american=american, ) return (calculated_price - market_price) ** 2 + result = minimize_scalar( binomial_objective_function, bounds=(0.001, 5.0), # Reasonable bounds for volatility - method='bounded' + method="bounded", ) - + return result.x if result.success else None - def estimate_crr_implied_volatility( - S: float, - K: float, - T: float, - r: float, - market_price: float, - q: float = 0.0, - option_type: str = 'c', - N: int = 1000, - dividend_type: Literal['continuous', 'discrete'] = 'continuous', - american: bool = False + S: float, + K: float, + T: float, + r: float, + market_price: float, + q: float = 0.0, + option_type: str = "c", + N: int = 1000, + dividend_type: Literal["continuous", "discrete"] = "continuous", + american: bool = False, ) -> float: """ Estimate implied volatility using optimization. - + Parameters: - S: Spot price - K: Strike price @@ -302,19 +606,30 @@ def estimate_crr_implied_volatility( - q: Continuous dividend yield (default is 0.0) - option_type: 'c' for call, 'p' for put - N: Number of time steps in the binomial tree - + Returns: - Estimated volatility """ + + S = _k(S) + K = _k(K) + T = _k(T, nd=6) + r = _k(r, nd=6) + market_price = _k(market_price) + q = _k(q, nd=6) if dividend_type == "continuous" else q + option_type = option_type + N = N + dividend_type = dividend_type + american = american return _estimate_crr_cached( - S=_k(S), - K=_k(K), - T=_k(T, nd=6), - r=_k(r, nd=6), - market_price=_k(market_price), - q=_k(q, nd=6) if dividend_type == 'continuous' else q, + S=S, + K=K, + T=T, + r=r, + market_price=market_price, + q=q, option_type=option_type, N=N, dividend_type=dividend_type, - american=american + american=american, ) diff --git a/trade/optionlib/vol/ssvi/utils.py b/trade/optionlib/vol/ssvi/utils.py index 92a7ea4..9834401 100644 --- a/trade/optionlib/vol/ssvi/utils.py +++ b/trade/optionlib/vol/ssvi/utils.py @@ -194,8 +194,8 @@ def get_chain(tick: str, date: str) -> pd.DataFrame: chain["log_moneyness"] = np.log(chain["moneyness"]) chain["T"] = chain["Expiration"].apply( lambda x: time_distance_helper( - x, - date, + end=x, + start=date, ) ) chain["T"] = chain["T"].astype(float)